From 5b5d2865a6e11ac7a07ad842092f6ac73c4d704e Mon Sep 17 00:00:00 2001 From: Rebecca Palmer Date: Fri, 6 Jun 2014 09:45:18 +0100 Subject: [PATCH] add script for checking for unused or dds-only textures --- textures_used_checker.py | 172 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 textures_used_checker.py diff --git a/textures_used_checker.py b/textures_used_checker.py new file mode 100644 index 0000000..38a5492 --- /dev/null +++ b/textures_used_checker.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +from __future__ import print_function#defaults to Python 3, but should also work in 2.7 + +"""Script for checking if any textures are unused (wasting space), and if any textures are only available as .dds (not recommended in the source repository, as it is a lossy-compressed format) + +Set basedir to your fg-root, and enable the kind(s) of output you want below:""" + +#basedir='/usr/share/games/flightgear/'#Debian/Ubuntu installed default +output_lists=True#prints lists of unused textures, and of dds-only textures +grep_check=False#checks for possible use outside the normal directories; requires Unix shell +output_rsync_rules=False#prints rsync rules for excluding unused textures from the release flightgear-data. Warning: if you use this, re-run this script regularly, in case they start being used +output_comparison_strips=False#creates thumbnail strips, unused_duplicate.png/unused_dds.png/high_low.png, for visually checking whether same-name textures are the same (remove the unused one entirely) or different (move it to Unused); requires ImageMagick/graphicsmagick +output_removal_commands=False#creates another script, delete_unused_textures.sh, which will remove unused textures when run in a Unix shell + +import os +import os.path +import re +from collections import defaultdict +import subprocess + +try: + basedir +except NameError: + print("basedir not set, please set it") + +def rfilelist(path,exclude_dirs=[]): + """Dict of files/sizes in path, including those in any subdirectories (as relative paths)""" + files=defaultdict(int) + dirs=[""] + while dirs: + cdir=dirs.pop() + cdirfiles=os.listdir(os.path.join(path,cdir)) + for file in cdirfiles: + if os.path.isdir(os.path.join(path,cdir,file)): + if os.path.join(cdir,file) not in exclude_dirs: + dirs.append(os.path.join(cdir,file)) + else: + files[os.path.join(cdir,file)]=os.path.getsize(os.path.join(path,cdir,file)) + return files +def textures_used(path,exclude_dirs=[],pattern=r'<(?:texture|object-mask|tree-texture).*?>(\S+?) (possibly with number), or element (for materials files)""" + textures=[] + matfiles=rfilelist(path,exclude_dirs).keys() + texfind=re.compile(pattern) + for file in matfiles: + f=open(os.path.join(path,file),'r') + for line in f: + tex=texfind.search(line) + if tex: + textures.append(tex.group(1)) + return textures +def image_check_strip(index_fname,ilist1,ilist2=None,size=128): + """Generate two rows of thumbnails, for easy visual comparison (between the two lists given, or if a single list is given, between low and high resolution)""" + if ilist2 is None: + ipairs=[[os.path.join(basedir,'Textures',f),os.path.join(basedir,'Textures.high',f)] for f in ilist1] + else: + ipairs=[] + for f1,f2 in zip(ilist1,ilist2): + if f1 in low_textures: + ipairs.append([os.path.join(basedir,'Textures',f1),os.path.join(basedir,'Textures',f2) if f2 in low_textures else os.path.join(basedir,'Textures.high',f2)]) + if f1 in high_textures: + ipairs.append([os.path.join(basedir,'Textures.high',f1),os.path.join(basedir,'Textures.high',f2) if f2 in high_textures else os.path.join(basedir,'Textures',f2)]) + ilist_f=[f[0] for f in ipairs]+[f[1] for f in ipairs] + subprocess.call(['montage','-label',"'%f'"]+ilist_f+['-tile','x2','-geometry',str(size)+'x'+str(size)]+[index_fname]) +def rsync_rules(flist,include=False,high=None): + """Output rsync rules to exclude/include the specified textures from high/low/both (high=True/False/None) resolutions""" + for f in flist: + if high!=True and f in low_textures: + print("+" if include else "-",os.path.join('/fgdata/Textures',f)) + if high!=False and f in high_textures: + print("+" if include else "-",os.path.join('/fgdata/Textures.high',f)) +def removal_command(flist,high=None): + """Return command to delete the specified textures from high/low/both (high=True/False/None) resolutions""" + a="rm" + for f in flist: + if high!=True and f in low_textures: + a=a+" "+os.path.join('Textures',f) + if high!=False and f in high_textures: + a=a+" "+os.path.join('Textures.high',f) + a=a+"\n" + return a +def move_command(flist,high=None,comment=False): + """Return command to move the specified textures to Unused from high/low/both (high=True/False/None) resolutions""" + dirset_low=set() if high==True else set(os.path.dirname(f) for f in set(flist)&low_textures) + dirset_high=set() if high==False else set(os.path.dirname(f) for f in set(flist)&high_textures) + a="" + for d in dirset_low: + a=a+("#" if comment else "")+"mv --target-directory="+os.path.join("Textures/Unused",d)+" "+(" ".join(os.path.join("Textures",f) for f in flist if (os.path.dirname(f)==d and f in low_textures)))+"\n" + for d in dirset_high: + a=a+("#" if comment else "")+"mv --target-directory="+os.path.join("Textures/Unused",d+".high")+" "+(" ".join(os.path.join("Textures.high",f) for f in flist if (os.path.dirname(f)==d and f in high_textures)))+"\n" + return a +false_positives=set(['buildings-lightmap.png','buildings.png','Credits','Globe/00README.txt', 'Globe/01READMEocean_depth_1png.txt', 'Globe/world.topo.bathy.200407.3x4096x2048.png','Trees/convert.pl'])#these either aren't textures, or are used where we don't check +used_textures=set(textures_used(basedir+'Materials'))|false_positives +used_textures_noregions=set(textures_used(basedir+'Materials',exclude_dirs=['regions']))|false_positives +used_effectslow=set(textures_used(basedir+'Effects',pattern=r'image.*?>[\\/]?Textures[\\/](\S+?)Textures[\\/](\S+?)), and Materials /, explicitly includes the Textures/ or Textures.high/ +used_effectshigh=set(textures_used(basedir+'Effects',pattern=r'image.*?>[\\/]?Textures.high[\\/](\S+?)Textures.high[\\/](\S+?) .dds swap; none found + print("\n\nUse of sourceless textures:") + subprocess.call(["grep","-r","-E","--exclude-dir=Aircraft","--exclude-dir=.git","-e","("+")|(".join(sourceless)+")","/home/palmer/fs_dev/git/fgdata","/home/palmer/fs_dev/git/flightgear","/home/palmer/fs_dev/git/simgear"]) +if output_rsync_rules: + print("\n\nFull flightgear-data:\n") + rsync_rules(unused) + rsync_rules(low_unneeded,high=False) + print("\n\nMinimal flightgear-data:\n") + rsync_rules(low_textures-used_noreg_low,high=False) + rsync_rules(high_textures-used_noreg_onlyhigh,high=True) +if output_removal_commands: + r_script=open('delete_unused_textures.sh','w') + r_script.write("cd "+basedir+"\n") + r_script.write("#Unused duplicates\n") + r_script.write(removal_command(unused_duplicate)) + r_script.write("#Unused .dds versions\n") + r_script.write(removal_command(unused_dds-unused_dds_matchhigh,high=False)) + r_script.write(removal_command(unused_dds-unused_dds_matchlow,high=True)) + r_script.write("#Unused reduced-resolution versions\n") + r_script.write(removal_command(low_unneeded_duplicate|(unused_other&high_textures&low_textures)-set(lowres_maybe_source),high=False)) + r_script.write("#Unused unique .png (move to Unused)\n") + r_script.write("\n".join(["mkdir -p Textures/Unused/"+d for d in ['Terrain','Terrain.winter','Trees','Terrain.high','Terrain.winter.high','Trees.high','Runway','Water']])+"\n") + r_script.write(move_command([f for f in unused_other&high_textures if (f[-4:]!=".dds" and f[:5]!="Signs" and f[:6]!="Runway")],high=True)) + r_script.write(move_command([f for f in (unused_other-high_textures)|low_unneeded_nondup if (f[-4:]!=".dds" and f[:5]!="Signs" and f[:6]!="Runway")],high=False)) + r_script.write("#Unused unique .dds\n") + r_script.write("#It is my opinion that these should go, but if you'd prefer to move them to Unused I won't argue further\n") + r_script.write(removal_command([f for f in (unused_other&high_textures)|unused_dds_matchlow if (f[-4:]==".dds" and f[:5]!="Signs" and f[:6]!="Runway")],high=True)) + r_script.write(removal_command([f for f in low_unneeded_nondup|unused_dds_matchhigh if f[-4:]==".dds"],high=False)) + r_script.write(move_command([f for f in (unused_other&high_textures)|unused_dds_matchlow if (f[-4:]==".dds" and f[:5]!="Signs" and f[:6]!="Runway")],high=True,comment=True)) + r_script.write(move_command([f for f in (unused_other-high_textures)|low_unneeded_nondup|unused_dds_matchhigh if (f[-4:]==".dds" and f[:5]!="Signs" and f[:6]!="Runway")],high=False,comment=True)) + r_script.close() +