#!/usr/bin/env python3 from __future__ import print_function#defaults to Python 3, but should also work in 2.7 import os import os.path import re from collections import defaultdict import subprocess import math 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 find_unused_textures(basedir,output_lists=True,grep_check=False,output_rsync_rules=False,output_comparison_strips=False,output_removal_commands=False): """Checks 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: output_lists prints lists of unused textures, and of dds-only textures grep_check checks for possible use outside the normal directories; requires Unix shell output_rsync_rules 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 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 creates another script, delete_unused_textures.sh, which will remove unused textures when run in a Unix shell""" 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(os.path.join(basedir,'Materials')))|false_positives used_textures_noregions=set(textures_used(os.path.join(basedir,'Materials'),exclude_dirs=['regions']))|false_positives used_effectslow=set(textures_used(os.path.join(basedir,'Effects'),pattern=r'image.*?>[\\/]?Textures[\\/](\S+?)Textures[\\/](\S+?)), and Materials /, explicitly includes the Textures/ or Textures.high/ used_effectshigh=set(textures_used(os.path.join(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(basedir,unused) rsync_rules(basedir,low_unneeded,high=False) print("\n\nMinimal flightgear-data:\n") rsync_rules(basedir,low_textures-used_noreg_low,high=False) rsync_rules(basedir,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(basedir,unused_duplicate)) r_script.write("#Unused .dds versions\n") r_script.write(removal_command(basedir,unused_dds-unused_dds_matchhigh,high=False)) r_script.write(removal_command(basedir,unused_dds-unused_dds_matchlow,high=True)) r_script.write("#Unused reduced-resolution versions\n") r_script.write(removal_command(basedir,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(basedir,[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(basedir,[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(basedir,[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(basedir,[f for f in (unused_other-high_textures)|low_unneeded_nondup|unused_dds_matchhigh if f[-4:]==".dds"],high=False)) r_script.write(move_command(basedir,[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(basedir,[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() def size_by_type(path,exclude_dirs=[]): """Dict of total file size by file extension""" files=rfilelist(path,exclude_dirs) size_totals=defaultdict(int) for filename,size in files.items(): file_ext=os.path.splitext(filename)[1] if file_ext==".gz": file_ext=os.path.splitext(os.path.splitext(filename)[0])[1]+file_ext size_totals[file_ext]=size_totals[file_ext]+size return size_totals def size_by_size(path,exclude_dirs=[],exts=[".png",".dds",".rgb"]): """Dict of total file size by individual file size range, of given extensions (empty list for all files)""" files=rfilelist(path,exclude_dirs) size_totals=defaultdict(int) for filename,size in files.items(): file_ext=os.path.splitext(filename)[1] if (not exts) or (file_ext in exts): size_totals[2**math.frexp(size)[1]]=size_totals[2**math.frexp(size)[1]]+size return size_totals def fgdata_size(path,dirs_to_list=["AI/Aircraft","AI/Traffic","Models","Scenery","Textures","Textures.high"],exclude_dirs=[".git","Aircraft"]): exclude_list=[[]]*len(dirs_to_list)+[dirs_to_list+exclude_dirs]+[exclude_dirs] names_list=dirs_to_list+["other","all"] for n,dir1 in enumerate(dirs_to_list+["",""]): size_totals=size_by_type(os.path.join(path,dir1),exclude_list[n]) print(names_list[n],sorted(size_totals.items(),key=lambda x:-x[1]),"total",sum(size_totals.values())) def create_reduced_fgdata(input_path,output_path,exclude_ai=False): """Create a smaller, reduced-quality flightgear-data package Requires imagemagick (not Ubuntu graphicsmagick, it can't write .dds)""" raise ValueError("The package this generates currently crashes FlightGear; not sure why yet") texture_filetypes=[".png",".rgb",".dds"] downsample_min_filesize=30000 dirs_to_downsample=("Textures.high/Terrain","Textures.high/Trees","Textures.high/Terrain.winter","AI/Aircraft","Models") exclude_dirs=[".git"] exclude_unnamed_subdirs=["Aircraft"] include_subdirs=["Aircraft/c172p","Aircraft/Generic","Aircraft/Instruments","Aircraft/Instruments-3d","Aircraft/ufo"] if exclude_ai: exclude_unnamed_subdirs.extend(["AI/Aircraft","AI/Traffic"]) subprocess.call(["mkdir","-p",output_path]) if os.path.exists(os.path.join(input_path,".git")): print(input_path,"appears to be a git clone; this will work, but the result will be slightly larger than starting from a standard flightgear-data package.\nTo create this use (adjusting paths as necessary) rsync -av --filter=\"merge /home/palmer/fs_dev/git/fgmeta/base-package.rules\" ~/fs_dev/git/fgdata ~/fs_dev/flightgear/data_full") if os.listdir(output_path): print("output path",output_path,"non-empty, aborting to avoid data loss\nIf you did want to lose its previous contents, run:\nrm -r",output_path,"\nthen re-run this script") return dirs=[""] while dirs: cdir=dirs.pop() cdirfiles=os.listdir(os.path.join(input_path,cdir)) for file in cdirfiles: if os.path.isdir(os.path.join(input_path,cdir,file)): if (os.path.join(cdir,file) not in exclude_dirs) and (cdir not in exclude_unnamed_subdirs or os.path.join(cdir,file) in include_subdirs): subprocess.call(["mkdir","-p",os.path.join(output_path,cdir,file)]) dirs.append(os.path.join(cdir,file)) else: if (cdir.startswith(dirs_to_downsample)) and (os.path.splitext(file)[1] in texture_filetypes) and (os.path.getsize(os.path.join(input_path,cdir,file))>downsample_min_filesize): subprocess.call(["convert",os.path.join(input_path,cdir,file),"-sample","50%",os.path.join(output_path,cdir,file)]) else: subprocess.call(["cp",os.path.join(input_path,cdir,file),os.path.join(output_path,cdir,file)])