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