1#!/usr/bin/env python3
2import struct
3# Xcursors Original mapping based on https://revadig.blogspot.com/2017/09/x11-xorg-set-mouse-pointer-cursor.html
4# Hashes/Extras: https://fedoraproject.org/wiki/Artwork/EchoCursors/NamingSpec
5# QT Cursor names: https://doc.qt.io/archives/qtjambi-4.5.2_01/com/trolltech/qt/gui/QCursor.html
6# GDK Cursor: https://developer.gnome.org/gdk3/stable/gdk3-Cursors.html
7# Freedesktop cursors (fd): https://www.freedesktop.org/wiki/Specifications/cursor-spec/
8import subprocess
9import os
10import shutil
11import sys
12
13# If you're parsing Microsoft Windows 95 default animated cursors, pass the '-s' argument to skip the ugly first frame
14
15cursors = {
16'X_cursor': {'Windows Default': False,
17                  'fd': [],
18                  'gdk': [],
19                  'hashes': ['X-cursor'],
20                  'name': 'Cursor Logo',
21                  'qt': [],
22                  'xcursors': ['X_cursor',
23                               'boat',
24                               'pirate',
25                               'sailboat',
26                               'shuttle',
27                               'spider, trek',
28                               'umbrella',
29                               'coffee_mug']},
30 'appstarting': {'Windows Default': True,
31                     'fd': ['progress'],
32                     'gdk': ['progress'],
33                     'hashes': ['08e8e1c95fe2fc01f976f1e063a24ccd',
34                                '3ecb610c1bf2410f44200f48c40d3599', '00000000000000020006000e7e9ffc3f'],
35                     'name': 'Working in Background',
36                     'qt': ['left_ptr_watch', 'half-busy'],
37                     'xcursors': []},
38 'arrow': {'Windows Default': True,
39               'fd': ['default'],
40               'gdk': ['default'],
41               'hashes': ['top-left-arrow'],
42               'name': 'Normal Select',
43               'qt': ['left_ptr'],
44               'xcursors': ['arrow',
45                            'draft_large',
46                            'draft_small',
47                            'left_ptr',
48                            'top_left_arrow']},
49 'bottom_tee': {'Windows Default': False,
50                    'fd': [],
51                    'gdk': [],
52                    'hashes': ['bottom_tee'],
53                    'name': 'Cell select bottom(?)',
54                    'qt': [],
55                    'xcursors': ['bottom_tee']},
56 'circle': {'Windows Default': False,
57                'fd': [],
58                'gdk': [],
59                'hashes': [],
60                'name': 'Circle? Bullseye?',
61                'qt': [],
62                'xcursors': ['circle', 'target']},
63 'clock': {'Windows Default': False,
64               'fd': [],
65               'gdk': [],
66               'hashes': [],
67               'name': 'Clock? Not used?',
68               'qt': [],
69               'xcursors': ['clock']},
70 'copy': {'Windows Default': False,
71              'fd': ['copy'],
72              'gdk': ['copy', 'grab', 'grabbing'],
73              'hashes': ['dnd-copy',
74                         '1081e37283d90000800003c07f3ef6bf',
75                         '6407b0e94181790501fd1e167b474872',
76                         '08ffe1cb5fe6fc01f906f1c063814ccf',
77                         '5aca4d189052212118709018842178c0',
78                         '208530c400c041818281048008011002',
79                         'fcf21c00b30f7e3f83fe0dfd12e71cff'],
80              'name': 'Drag and drop copy',
81              'qt': [],
82              'xcursors': []},
83 'cross': {'Windows Default': False,
84               'fd': [],
85               'gdk': [],
86               'hashes': [],
87               'name': 'Crosshair',
88               'qt': [],
89               'xcursors': ['crosshair', 'cross_reverse']},
90 'crosshair': {'Windows Default': True,
91                   'fd': ['crosshair'],
92                   'gdk': ['crosshair'],
93                   'hashes': [],
94                   'name': 'Precision select',
95                   'qt': ['cross'],
96                   'xcursors': ['cross',
97                                'diamond_cross',
98                                'iron_cross',
99                                'tcross']},
100 'dotbox': {'Windows Default': False,
101                'fd': [],
102                'gdk': [],
103                'hashes': ['dot-box', 'dot_box', 'dot_box_mask'],
104                'name': 'dot in a box(?)',
105                'qt': [],
106                'xcursors': ['dotbox',
107                             'dot',
108                             'bogosity',
109                             'box_spiral',
110                             'draped_box',
111                             'heart, icon',
112                             'rtl_logo']},
113 'exchange': {'Windows Default': False,
114                  'fd': [],
115                  'gdk': [],
116                  'hashes': [],
117                  'name': 'Clock? Not used?',
118                  'qt': [],
119                  'xcursors': ['exchange']},
120 'gumby': {'Windows Default': False,
121               'fd': [],
122               'gdk': [],
123               'hashes': [],
124               'name': 'Fun characters',
125               'qt': [],
126               'xcursors': ['gumby', 'gobbler', 'man']},
127 'hand1': {'Windows Default': False,
128               'fd': ['pointer'],
129               'gdk': ['pointer'],
130               'hashes': ['9d800788f1b08800ae810202380a0822',
131                          'e29285e634086352946a0e7090d73106',
132                          'hand', 'HandGrab', 'HandSqueezed'],
133               'name': 'Hand pointer',
134               'qt': ['pointing_hand', 'openhand'],
135               'xcursors': ['hand1', 'hand2']},
136 'help': {'Windows Default': True,
137              'fd': ['help', 'context-menu'],
138              'gdk': ['help', 'context-menu'],
139              'hashes': ['ask', 'dnd-ask', 'd9ce0ab605698f320427677b458ad60b',
140                                           '5c6cd98b3f3ebcb1f9c7f1c204630408'],
141              'name': 'Help Select',
142              'qt': ['whats_this'],
143              'xcursors': ['question_arrow']},
144 'ibeam': {'Windows Default': True,
145               'fd': ['text'],
146               'gdk': ['text'],
147               'hashes': [],
148               'name': 'Text Select',
149               'qt': ['ibeam'],
150               'xcursors': ['xterm']},
151 'left_tee': {'Windows Default': False,
152                  'fd': [],
153                  'gdk': [],
154                  'hashes': ['left_tee'],
155                  'name': 'Cell select bottom(?)',
156                  'qt': [],
157                  'xcursors': ['left_tee']},
158 'link': {'Windows Default': False,
159              'fd': ['link'],
160              'gdk': ['alias'],
161              'hashes': ['3085a0e285430894940527032f8b26df',
162                         '640fb0e74195791501fd1ed57b41487f',
163                         'a2a266d0498c3104214a47bd64ab0fc8',
164                         '0876e1c15ff2fc01f906f1c363074c0f', 'dnd-link'],
165              'name': 'Create link',
166              'qt': ['closedhand'],
167              'xcursors': []},
168 'mouse': {'Windows Default': False,
169               'fd': [],
170               'gdk': [],
171               'hashes': [],
172               'name': 'Mouse demo',
173               'qt': [],
174               'xcursors': ['mouse', 'middlebutton', 'rightbutton']},
175 'move': {'Windows Default': False,
176              'fd': [],
177              'gdk': [],
178              'hashes': ['move',
179                         'dnd-move',
180                         '4498f0e0c1937ffe01fd06f973665830',
181                         '9081237383d90e509aa00f00170e968f'],
182              'name': 'color picker',
183              'qt': [],
184              'xcursors': []},
185 'no': {'Windows Default': True,
186            'fd': ['no-drop', 'not-allowed'],
187            'gdk': ['no-drop', 'not-allowed'],
188            'hashes': ['dnd-none',
189                       '03b6e0fcb3499374a867c041f52298f0',
190                       'crossed_circle'],
191            'name': 'Unavailable/Forbidden',
192            'qt': ['forbidden', 'dnd-no-drop'],
193            'xcursors': []},
194 'pen': {'Windows Default': True,
195             'fd': [],
196             'gdk': [],
197             'hashes': [],
198             'name': 'Handwriting',
199             'qt': [],
200             'xcursors': ['pencil']},
201 'picker': {'Windows Default': False,
202                'fd': [],
203                'gdk': [],
204                'hashes': ['picker'],
205                'name': 'color picker',
206                'qt': ['color-picker'],
207                'xcursors': []},
208 'plus': {'Windows Default': False,
209              'fd': ['cell'],
210              'gdk': ['cell'],
211              'hashes': [],
212              'name': 'Add a cell',
213              'qt': [],
214              'xcursors': ['plus']},
215 'right_ptr': {'Windows Default': False,
216                   'fd': [],
217                   'gdk': [],
218                   'hashes': [],
219                   'name': 'Right pointer',
220                   'qt': [],
221                   'xcursors': ['right_ptr']},
222 'right_tee': {'Windows Default': False,
223                   'fd': [],
224                   'gdk': [],
225                   'hashes': ['right_tee'],
226                   'name': 'Cell select right(?)',
227                   'qt': [],
228                   'xcursors': ['right_tee']},
229 'sb_h_double_arrow': {'Windows Default': False,
230                           'fd': [],
231                           'gdk': [],
232                           'hashes': ['14fef782d02440884392942c11205230',
233                                      'h_double_arrow', '028006030e0e7ebffc7f7070c0600140',
234                                      'right', 'HDoubleArrow'],
235                           'name': 'Resize area between panels',
236                           'qt': ['split_h'],
237                           'xcursors': ['sb_h_double_arrow',
238                                        'sb_left_arrow',
239                                        'sb_right_arrow']},
240 'sb_v_double_arrow': {'Windows Default': False,
241                           'fd': [],
242                           'gdk': [],
243                           'hashes': ['2870a09082c103050810ffdffffe0204',
244                                      'v_double_arrow', 'VDoubleArrow'],
245                           'name': 'Resize area between panels',
246                           'qt': ['split_v'],
247                           'xcursors': ['sb_v_double_arrow',
248                                        'sb_up_arrow',
249                                        'sb_down_arrow']},
250 'sizeall': {'Windows Default': True,
251                 'fd': ['all-scroll'],
252                 'gdk': ['move', 'all-scroll'],
253                 'hashes': [],
254                 'name': 'Move',
255                 'qt': ['size_all'],
256                 'xcursors': ['fleur']},
257 'sizenesw': {'Windows Default': True,
258                  'fd': ['ne-resize', 'sw-resize', 'nesw-resize'],
259                  'gdk': ['ne-resize', 'sw-resize', 'nesw-resize'],
260                  'hashes': ['bd_double_arrow',
261                             'fcf1c3c7cd4491d801f1e1c78f100000'],
262                  'name': 'Diagonal resize 2',
263                  'qt': ['size_bdiag'],
264                  'xcursors': ['bottom_left_corner',
265                               'll_angle',
266                               'top_right_corner',
267                               'ur_angle']},
268 'sizens': {'Windows Default': True,
269                'fd': ['row-resize', 'n-resize', 's-resize', 'ns-resize'],
270                'gdk': ['row-resize', 'n-resize', 's-resize', 'ns-resize'],
271                'hashes': ['base_arrow_down',
272                           'base_arrow_up',
273                           'v_double_arrow',
274                           '00008160000006810000408080010102'],
275                'name': 'Vertical resize',
276                'qt': ['size_ver'],
277                'xcursors': ['based_arrow_down',
278                             'based_arrow_up',
279                             'double_arrow',
280                             'bottom_side',
281                             'top_side']},
282 'sizenwse': {'Windows Default': True,
283                  'fd': ['nw-resize', 'se-resize', 'nwse-resize'],
284                  'gdk': ['nw-resize', 'se-resize', 'nwse-resize'],
285                  'hashes': ['fd_double_arrow',
286                             'c7088f0f3e6c8088236ef8e1e3e70000'],
287                  'name': 'Diagonal resize 1',
288                  'qt': ['size_fdiag'],
289                  'xcursors': ['top_left_corner',
290                               'ul_angle',
291                               'lr_angle',
292                               'bottom_right_corner',
293                               'sizing']},
294 'sizewe': {'Windows Default': True,
295                'fd': ['col-resize', 'e-resize', 'w-resize', 'ew-resize'],
296                'gdk': ['col-resize', 'e-resize', 'w-resize', 'ew-resize'],
297                'hashes': [],
298                'name': 'Horizontal resize',
299                'qt': ['size_hor'],
300                'xcursors': ['left_side', 'right_side']},
301 'spraycan': {'Windows Default': False,
302                  'fd': [],
303                  'gdk': [],
304                  'hashes': [],
305                  'name': 'Grafiti time',
306                  'qt': [],
307                  'xcursors': ['spraycan']},
308 'star': {'Windows Default': False,
309              'fd': [],
310              'gdk': [],
311              'hashes': [],
312              'name': 'Star time',
313              'qt': [],
314              'xcursors': ['star']},
315 'top_tee': {'Windows Default': False,
316                 'fd': [],
317                 'gdk': [],
318                 'hashes': ['top_tee'],
319                 'name': 'Cell select top(?)',
320                 'qt': [],
321                 'xcursors': ['top_tee']},
322 'uparrow': {'Windows Default': True,
323                 'fd': ['up-arrow'],
324                 'gdk': [],
325                 'hashes': ['basic-arrow', 'bassic_arrow'],
326                 'name': 'Alternate Select',
327                 'qt': ['up_arrow'],
328                 'xcursors': ['center_ptr']},
329 'vertical-text': {'Windows Default': False,
330                       'fd': ['vertical-text'],
331                       'gdk': ['vertical-text'],
332                       'hashes': [],
333                       'name': 'Vertical text selector',
334                       'qt': [],
335                       'xcursors': []},
336 'wait': {'Windows Default': True,
337              'fd': ['wait'],
338              'gdk': ['wait'],
339              'hashes': [],
340              'name': 'Busy',
341              'qt': ['wait'],
342              'xcursors': ['watch']},
343 'zoom-in': {'Windows Default': False,
344                 'fd': [],
345                 'gdk': ['zoom-in'],
346                 'hashes': ['f41c0e382c94c0958e07017e42b00462', 'zoomIn'],
347                 'name': 'Zoom in',
348                 'qt': [],
349                 'xcursors': []},
350 'zoom-out': {'Windows Default': False,
351                  'fd': [],
352                  'gdk': ['zoom-out'],
353                  'hashes': ['f41c0e382c97c0938e07017e42800402', 'zoomOut',
354                             'a2a266d0498c3104214a47bd64ab0fc8'],
355                  'name': 'Drag and drop copy',
356                  'qt': [],
357                  'xcursors': []}
358}
359
360
361
362def extract_cur(file_name):
363	print("\tParsing cursor file {}".format(file_name))
364	# input: .cur file location/name
365	# output: dict with cursor information
366
367	f = open(file_name,'rb')
368	cur_file = f.read()
369	f.close()
370	cur_bytes = bytearray(cur_file)
371	rtIconDir = False
372	rtIconDirEntry = False
373	INFO = False
374	icon = []
375
376	icon.append({
377		'rtIconDir' : {
378		'res' : struct.unpack('<H',cur_bytes[0:2])[0],
379		'ico_type' : struct.unpack('<H',cur_bytes[2:4])[0],
380		'ico_num_images' : struct.unpack('<H',cur_bytes[4:6])[0]
381		},
382		'ico_file' : cur_bytes,
383		#ICONDIRENTRY
384		# TODO Add multiple cursors here if needed like icons
385		'rtIconDirEntry' : {
386			'bWidth'       : cur_bytes[6], # Width, in pixels, of the image
387			'bHeight'      : cur_bytes[7], # Height, in pixels, of the image
388			'bColorCount'  : cur_bytes[8], # Number of colors in image (0 if >=8bpp)
389			'bReserved'    : cur_bytes[9], # Reserved
390			'wPlanes'      : struct.unpack('<H',cur_bytes[10:12])[0], # Color Planes
391			'wBitCount'    : struct.unpack('<H',cur_bytes[12:14])[0], # Bits per pixel
392			'dwBytesInRes' : struct.unpack('<L',cur_bytes[14:18])[0], # how many bytes in this resource?
393			'dwDIBOffset'  : struct.unpack('<H',cur_bytes[18:20])[0] # RT_ICON rnID
394		}
395
396	})
397
398	cursor = {
399		'icon' : icon
400	}
401
402	return cursor
403
404def convert_cur_files(cursor_filename, output_file_name):
405	print("\tConverting {} to {}".format(cursor_filename, output_file_name))
406	convert_path = subprocess.check_output(["which", "convert"]).strip()
407	args = [
408	convert_path,
409	cursor_filename,
410	output_file_name
411	]
412	subprocess.check_call(args)
413	if  os.path.isfile(output_file_name[:-4]+"-0.png"):
414		shutil.move(output_file_name[:-4]+"-1.png", output_file_name[:-4]+".png")
415		os.remove(output_file_name[:-4]+"-0.png")
416
417
418def extract_ani(file_name):
419	print("\tParsing ani file {}".format(file_name))
420	f = open(file_name,'rb')
421	ani_file = f.read()
422	f.close()
423	ani_bytes = bytearray(ani_file)
424
425	rate = False
426	seq = False
427	rtIconDir = False
428	rtIconDirEntry = False
429	INFO = False
430	anih = False
431	icon = []
432	icon_count = 0
433
434	ckID   = ani_bytes[0:4].decode()
435	ckSize =  struct.unpack('<L',ani_bytes[4:8])[0]
436	ckForm = ani_bytes[8:12].decode()
437
438	total_size = 0
439	print("{:<21} | Extracting cursors/icons from ani file: {}".format("", file_name))
440
441	# ANI files are just RIFF files
442	if ckID == 'RIFF':
443		print("{:<21} | {} ckSize :{}".format("","RIFF detected", ckSize))
444		if ckForm == 'ACON': #ACON is optional
445			#print("ACON detected (optional)")
446			total_size = 12 # RIFF Header with ACON
447		else:
448			total_size = 8 # RIFF Header without ACON
449
450		if ckSize == len(ani_bytes) - 8:
451			ckSize = ckSize + 8 # Sometimes, but not always, the header isn't included in ckSize
452			print("{:<21} | Adjusting ckSize to actual file size: {}".format("",ckSize))
453
454		while total_size < ckSize:
455			section = ani_bytes[total_size:total_size+4].decode()
456			total_size = total_size + 4
457			chunk_size = struct.unpack('<L',ani_bytes[total_size:total_size+4])[0]
458			total_size = total_size + 4
459
460			#print("Chunk {}, Size: {}".format(section, chunk_size))
461			#print(ani_bytes[total_size:total_size+36])
462			if section == 'anih': #ANI Header
463				print("{:<21} | Chunk: anih".format(""))
464				anih = {
465					'cbSize': struct.unpack('<L',ani_bytes[total_size:total_size+4])[0],
466					'nFrames': struct.unpack('<L',ani_bytes[total_size+4:total_size+8])[0],
467					'nSteps' : struct.unpack('<L',ani_bytes[total_size+8:total_size+12])[0],
468					'iWidth' : struct.unpack('<L',ani_bytes[total_size+12:total_size+16])[0],
469					'iHeight' : struct.unpack('<L',ani_bytes[total_size+16:total_size+20])[0],
470					'iBitCount' : struct.unpack('<L',ani_bytes[total_size+20:total_size+24])[0],
471					'nPlanes' : struct.unpack('<L',ani_bytes[total_size+24:total_size+28])[0],
472					'iDispRate' : struct.unpack('<L',ani_bytes[total_size+28:total_size+32])[0], # The value is expressed in 1/60th-of-a-second units, which are known as jiffie, ignored if seq exists
473					'bfAttributes' : struct.unpack('<L',ani_bytes[total_size+32:total_size+36])[0]
474				}
475			elif section == 'rate':
476				print("{:<21} | Chunk: rate, size: {}".format("", chunk_size))
477				rate = []
478				for jiffie in range(0,chunk_size,4):
479					rate.append(struct.unpack('<L',ani_bytes[total_size+jiffie:total_size+jiffie+4])[0])
480			elif section == 'seq ':
481				print("{:<21} | Chunk: seq, size: {}".format("",chunk_size))
482				seq = []
483				for sequence in range(0,chunk_size,4):
484					seq.append(struct.unpack('<L',ani_bytes[total_size+sequence:total_size+sequence+4])[0])
485				# bfAttributes: 1 == CUR or ICO, 0 == BMP, 3 == 'seq' block is present
486			elif section == 'LIST':
487				chunk_type = ani_bytes[total_size:total_size+4].decode()
488				LIST_item_size = total_size + 4
489				print("{:<21} | Chunk: {}, size: {}".format("",chunk_type, chunk_size))
490				if chunk_type == 'INFO':
491					INFO = {}
492					while LIST_item_size <= chunk_size:
493						try:
494							info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
495							list_chunk_size = struct.unpack('<L',ani_bytes[LIST_item_size+4:LIST_item_size+8])[0]
496							INFO[info_section] = ani_bytes[LIST_item_size+8:LIST_item_size+8+list_chunk_size].decode()
497						except UnicodeDecodeError:
498							info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode('latin-1')
499							list_chunk_size = struct.unpack('<L',ani_bytes[LIST_item_size+4:LIST_item_size+8])[0]
500							INFO[info_section] = ani_bytes[LIST_item_size+8:LIST_item_size+8+list_chunk_size].decode('latin-1')
501
502
503						if (list_chunk_size % 2) != 0: # Yay DWORD boundaries
504							list_chunk_size = list_chunk_size + 1
505						LIST_item_size = LIST_item_size + list_chunk_size + 8
506				elif chunk_type == 'fram':
507					info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
508					while LIST_item_size < chunk_size:
509						print("{:<21} | Chunk: {}, size: {}".format("",info_section, LIST_item_size))
510						info_section = ani_bytes[LIST_item_size:LIST_item_size+4].decode()
511						list_chunk_size = struct.unpack('<L',ani_bytes[LIST_item_size+4:LIST_item_size+8])[0]
512						if info_section == 'icon':
513							icon.append({
514								'index' : icon_count,
515								#ICONDIR
516								'rtIconDir' : {
517								'res' : struct.unpack('<H',ani_bytes[LIST_item_size+8:LIST_item_size+10])[0],
518								'ico_type' : struct.unpack('<H',ani_bytes[LIST_item_size+10:LIST_item_size+12])[0],
519								'ico_num_images' : struct.unpack('<H',ani_bytes[LIST_item_size+12:LIST_item_size+14])[0]
520								},
521								#ICONDIRENTRY
522								'rtIconDirEntry' : {
523								'bWidth'       : ani_bytes[LIST_item_size+14], # Width, in pixels, of the image
524								'bHeight'      : ani_bytes[LIST_item_size+15], # Height, in pixels, of the image
525								'bColorCount'  : ani_bytes[LIST_item_size+16], # Number of colors in image (0 if >=8bpp)
526								'bReserved'    : ani_bytes[LIST_item_size+17], # Reserved
527								'wPlanes'      : struct.unpack('<H',ani_bytes[LIST_item_size+18:LIST_item_size+20])[0], # Color Planes (or hotspot X coords for cur)
528								'wBitCount'    : struct.unpack('<H',ani_bytes[LIST_item_size+20:LIST_item_size+22])[0], # Bits per pixel
529								'dwBytesInRes' : struct.unpack('<L',ani_bytes[LIST_item_size+22:LIST_item_size+26])[0], # how many bytes in this resource?
530								'dwDIBOffset'  : struct.unpack('<H',ani_bytes[LIST_item_size+26:LIST_item_size+28])[0] # RT_ICON rnID
531								},
532								'ico_file' : ani_bytes[LIST_item_size+8:LIST_item_size + list_chunk_size + 8]
533							})
534						icon_count += 1
535						#print(info_section, hex(LIST_item_size), list_chunk_size)
536						if (list_chunk_size % 2) != 0: # Yay DWORD boundaries
537							list_chunk_size = list_chunk_size + 1
538						LIST_item_size = LIST_item_size + list_chunk_size + 8
539
540
541			total_size = total_size + chunk_size # The 8 accounts for the chunk id and size which is not included in the size
542
543
544	else:
545		print("No RIFF ID, is {} an ANI file?".format(file_name))
546		print("{:<21} | RIFF ID: {}, Form: {}".format("", ckID, ckForm))
547
548
549	if INFO:
550		for i in INFO:
551			print("{:<21} | {:<21} | {}".format("",i, INFO[i]))
552	if anih:
553		for i in anih:
554			print("{:<21} | {:<21} | {}".format("",i, anih[i]))
555	for section in icon:
556			if section['index']:
557				print("{:<21} | Index: {}".format("",section['index']))
558				print("{:<21} | rtIconDir".format(""))
559				for j in section['rtIconDir']:
560					print("{:<21} | {:<21} | {}".format("",j, section['rtIconDir'][j]))
561				print("{:<21} | rtIconDirEntry".format(""))
562				for j in section['rtIconDirEntry']:
563					print("{:<21} | {:<21} | {}".format("",j, section['rtIconDirEntry'][j]))
564
565	cursor = {
566		'INFO' : INFO,
567		'anih' : anih,
568		'seq'  : seq,
569		'rate' : rate,
570		'icon' : icon
571	}
572
573	return cursor
574
575
576if os.path.exists('tmp'):
577	shutil.rmtree('tmp')
578
579os.makedirs('tmp')
580
581for cursor in cursors:
582	folder = 'xcursors'
583
584	print("Cursor: {}\n\tDesc: {}".format(cursor,cursors[cursor]['name']))
585
586	if cursors[cursor]['Windows Default']:
587		folder = '95'
588
589	if os.path.exists("{}/{}.cur".format(folder,cursor)):
590		ext = ".cur"
591	elif os.path.exists("{}/{}.ani".format(folder,cursor)):
592		ext = ".ani"
593	elif os.path.exists("{}/{}.ico".format(folder,cursor)):
594		ext = ".ico"
595	else:
596		print("{icon}.cur/{icon}.ani/{icon}.ico could not be found. Please place one of {icon}.cur/{icon}.ani/{icon}.ico in {folder}".format(icon=cursor,folder=folder))
597
598	if ext in ['.ico', '.cur']:
599		cursor_file_config = extract_cur(folder+"/"+cursor+ext)
600		xhot = cursor_file_config['icon'][0]['rtIconDirEntry']['wPlanes']
601		yhot = cursor_file_config['icon'][0]['rtIconDirEntry']['wBitCount']
602		size = cursor_file_config['icon'][0]['rtIconDirEntry']['bHeight']
603		icon_file = cursor_file_config['icon'][0]['ico_file']
604		f = open("tmp/"+cursor+ext,"wb")
605		f.write(icon_file)
606		f.close()
607
608		convert_cur_files("tmp/"+cursor+ext, "tmp/"+cursor+".png")
609		write_conf = open("tmp/"+cursor+".conf", 'w')
610		print("\tWritting conf file {}: {size} {xhot} {yhot} {filename}".format("tmp/"+cursor+".conf", size=size, xhot=xhot, yhot=yhot, filename=cursor+".png"))
611		write_conf.write("{size} {xhot} {yhot} {filename}".format(size=size, xhot=xhot, yhot=yhot, filename=cursor+".png"))
612		write_conf.close()
613		os.remove("tmp/"+cursor+ext)
614
615	elif ext == '.ani':
616		ani_file_config = extract_ani(folder+"/"+cursor+ext)
617		#pprint(ani_file_config)
618		print("{:<21} | Header - nFrames: {}, nSteps: {}, iDispRate: {}".format(cursor+ext, ani_file_config['anih']['nFrames'], ani_file_config['anih']['nSteps'], ani_file_config['anih']['iDispRate']))
619		write_conf = open("tmp/"+cursor+".conf", 'w')
620
621		if ani_file_config['seq']:
622			for sequence in ani_file_config['seq']:
623				if ani_file_config['rate']:
624					rate = ani_file_config['rate'][sequence] * 17
625				else:
626					rate = ani_file_config['anih']['iDispRate'] * 17
627
628				for icon in ani_file_config['icon']:
629					if icon['index'] == sequence:
630						xhot = icon['rtIconDirEntry']['wPlanes']
631						yhot = icon['rtIconDirEntry']['wBitCount']
632						size = icon['rtIconDirEntry']['bHeight']
633						print("{:<21} | Sequence: {}, rate: {}, size: {}, xhot: {}, yhot: {}".format(cursor+ext, sequence, rate, size,xhot, yhot))
634						cur_filename = cursor+"_"+str(sequence)
635						f = open("tmp/"+cur_filename+".cur","wb")
636						f.write(icon['ico_file'])
637						f.close()
638						convert_cur_files("tmp/"+cur_filename+".cur", "tmp/"+cur_filename+".png")
639						write_conf.write("{size} {xhot} {yhot} {filename} {rate}\n".format(size=size, xhot=xhot, yhot=yhot, filename=cur_filename+".png", rate=rate ))
640		else:
641			itericons = iter(ani_file_config['icon'])
642			# This is just for the default windows icons, no idea why
643			if len(sys.argv) > 1 and sys.argv[1] == '-s':
644				next(itericons)
645			for icon in itericons:
646				xhot = icon['rtIconDirEntry']['wPlanes']
647				yhot = icon['rtIconDirEntry']['wBitCount']
648				size = icon['rtIconDirEntry']['bHeight']
649				rate = ani_file_config['anih']['iDispRate'] * 17
650				print("{:<21} |  Sequence: {}, rate: {}, size: {}, xhot: {}, yhot: {}".format(cursor+ext, icon['index'], rate, size,xhot, yhot))
651				cur_filename = cursor+"_"+str(icon['index'])
652				f = open("tmp/"+cur_filename+".cur","wb")
653				f.write(icon['ico_file'])
654				f.close()
655				convert_cur_files("tmp/"+cur_filename+".cur", "tmp/"+cur_filename+".png")
656				write_conf.write("{size} {xhot} {yhot} {filename} {rate}\n".format(size=size, xhot=xhot, yhot=yhot, filename=cur_filename+".png", rate=rate))
657
658		for icon in ani_file_config['icon']:
659			xhot = icon['rtIconDirEntry']['wPlanes']
660			yhot = icon['rtIconDirEntry']['wBitCount']
661			size = icon['rtIconDirEntry']['bHeight']
662			#print(xhot, yhot, size)
663		write_conf.close()
664
665	print("\tConversion complete\n\tBuilding xcusorfiles in ../cursors")
666	for linux_type in ['xcursors', 'qt', 'fd', 'gdk', 'hashes']:
667		for output in cursors[cursor][linux_type]:
668			print("\t\t{}".format(output))
669			xcursorgen_path = subprocess.check_output(["which", "xcursorgen"]).strip()
670			args = [
671				xcursorgen_path,
672				"-p",
673				'tmp',
674				"tmp/"+cursor+".conf",
675				"../cursors/"+output
676			]
677			subprocess.check_call(args, stdout=subprocess.DEVNULL)
678shutil.rmtree('tmp')
679
680
681