#!/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()