#!/usr/bin/env python # ==================================================================== def ensure_list(what): if not what: return None if isinstance(what,str): return [what] return what # -------------------------------------------------------------------- def clean(base, suffixes): from pathlib import Path if not isinstance(base,Path): base = Path(base) for suffix in suffixes: tgt = base.with_suffix(suffix) tgt.unlink(missing_ok=True) # ==================================================================== possible_commands = ['air', 'land', 'equipment', 'installation', 'sea surface', 'sub surface', 'space', 'activity', 'dismounted'] possible_factions = ['friendly', 'hostile', 'neutral', 'unknown'] echelon_mapping = {'': 'team', '*': 'squad', '**': 'section', '***': 'platoon', '|': 'company', '||': 'battalion', '|||': 'regiment', 'x': 'brigade', 'xx': 'division', 'xxx': 'corps', 'xxxx': 'army', 'xxxxx': 'army group', 'xxxxxx': 'theater'} possible_echelons = list(echelon_mapping.values())+list(echelon_mapping.keys()) # ==================================================================== def make_picture(command, faction, echelon, main, top=None, bottom=None, left=None, right=None, below=None, decoy=False, line_width='1pt', scale=.45, more=None, **kwargs): '''Create latex code for the NATO App6 symbol - a single tikzpicture Parameters ---------- command : str Command (base symbol) faction : str Faction (base symbol) echelon : str or None Echelon (top of base symbol main : str or list of str Main symbols top : str or list of str Top modifiers bottom : str or list of str Bottom modifiers left : str or list of str Left modifiers right : str or list of str Right modifiers below : str or list of str Below base frame decoy : bool True if it should be a decoy line_width : str Line width, including unit scale : float Scaling of image Return ------ out : str LaTeX code to write to file ''' ind = ' ' def add_line(out,key,what,ind=ind): '''Adds a line with a key''' lwhat = ensure_list(what) if not lwhat: return out swhat = ",".join(lwhat) return out+f'{ind} {key}={{{swhat}}},%'+'\n' out = fr'''\begin{{tikzpicture}}% \natoapp[% scale={scale},% scale line widths,% line width={line_width},% command={command},% faction={faction},%'''+'\n' out = add_line(out,'echelon',echelon) out = add_line(out,'main', main) out = add_line(out,'top', top) out = add_line(out,'bottom', bottom) out = add_line(out,'left', left) out = add_line(out,'right', right) out = add_line(out,'below', below) if more: out += f'{ind} {more}%'+'\n' out += fr''' ];% \end{{tikzpicture}}% ''' lout = out.split('\n') lout = [l[len(ind):] if l.startswith(ind) else l for l in lout] return '\n'.join(lout) # -------------------------------------------------------------------- def make_latex(db): '''Create latex code for the all NATO App6 symbols - a complete document Parameters ---------- db : list of dict Configurations of each symbol Return ------ out : str LaTeX code to write to file ''' ind = ' ' out = r'''\documentclass[tikz,11pt]{standalone} \usepackage{wargame} \begin{document}%'''+'\n' for entry in db: out += make_picture(**entry) out += r'\end{document}' lout = out.split('\n') lout = [l[len(ind):] if l.startswith(ind) else l for l in lout] return '\n'.join(lout) # -------------------------------------------------------------------- def make_name(command, faction, echelon, main, top=None, bottom=None, left=None, right=None, below=None, decoy=False, output=None, **kwargs): '''Create the file name Parameters ---------- command : str Command (base symbol) faction : str Faction (base symbol) echelon : str or None Echelon (top of base symbol main : str or list of str Main symbols top : str or list of str Top modifiers bottom : str or list of str Bottom modifiers left : str or list of str Left modifiers right : str or list of str Right modifiers decoy : bool True if it should be a decoy Return ------ out : str Output file name ''' if output: return output.format(command=command, faction=faction, echelon=echelon)\ .replace(' ','-')\ .replace('=','-') def clean_sub(what): from re import sub res = what while True: old = res res = sub(r'\[[^]]+\]','',res) res = sub(r'\{[^}]+\}','',res) if res == old: break return res def add_component(out, what,sep='-'): lwhat = ensure_list(what) if not lwhat: return out return out+'_'+sep.join([clean_sub(l) for l in lwhat]) def add_front(out,what,sep='_'): if not what: return out if len(out) > 0: out += sep return out + what out = '' out = add_front(out,faction) out = add_front(out,command.replace(' ','-')) out = add_front(out,echelon.replace(' ','-')) out = add_component(out,main) out = add_component(out,top) out = add_component(out,bottom) out = add_component(out,left) out = add_component(out,right) out = add_component(out,below) return out.replace(' ','-').replace('=','-') # ==================================================================== default_latex_flags = ['-interaction=nonstopmode', '-file-line-error', '-synctex','15', '-shell-escape'] default_latex_command = 'pdflatex' # -------------------------------------------------------------------- def create_process(args,verbose=False): '''Create a subprocess with arguments Returns ------- proc : Popen Process with stdout and stderr as pipes (fifos) ''' from os import environ from subprocess import Popen, PIPE if verbose: print(f'Will run: "{" ".join(args)}"') return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE) # -------------------------------------------------------------------- default_timeout = None default_keep = False # -------------------------------------------------------------------- def run_latex(file, content, latex_cmd = default_latex_command, latex_flags = default_latex_flags, timeout = default_timeout, keep = default_keep, verbose = False): '''Run latex on generated content Parameters ---------- filename : str The file to write content : str Content to write to source file latex_cmd : str LaTeX command to run e.g., 'pdflatex' latex_flags : list of str Flags to pass to the LaTeX command line timeout : int Process timeout keep : bool If true, keep intermediate files Returns ------- None ''' from pathlib import Path fpath = Path(file.name).with_suffix('') csuf = ['.aux','.log','.out','.synctex','.synctex.gz','.tex'] file.write(content) file.close() lflags = ensure_list(latex_flags) args = [latex_cmd] + (lflags if lflags else + []) + \ [fpath.with_suffix('.tex').name] proc = create_process(args,verbose=verbose) try: out, err = proc.communicate(timeout=timeout) sout = out.decode() if 'Error' in sout or \ 'Emergency stop' in sout or \ not Path(fpath).with_suffix('.pdf').is_file(): raise RuntimeError(sout) except Exception as e: proc.kill() proc.communicate() if not keep: clean(base=fpath,suffixes=csuf+['.pdf']) raise RuntimeError(f'Failed on "{" ".join(args)}" with:\n'+ str(e) + '\n--- Start LaTeX code -------\n'+ content+ '\n--- End LaTeX code ---------') if not keep: clean(base=fpath,suffixes=csuf) # -------------------------------------------------------------------- possible_formats = ['png','svg','jpg','tiff'] default_format = possible_formats[0] default_resolution = 150 # -------------------------------------------------------------------- def run_cairo(filename, npages, format = default_format, resolution = default_resolution, timeout = default_timeout, keep = default_keep, verbose = False): '''Run 'pdftocairo' on generated PDF Parameters ---------- filename : str The file to write format : str Image format to write resolution : int Pixels Per Inch (PPI) timeout : int Process timeout keep : bool If true, keep intermediate files Returns ------- None ''' from pathlib import Path fpath = Path(filename) args = ['pdftocairo'] if format != 'svg': args.extend(['-transp']) args.extend(['-r',str(resolution),f'-{format}']) inpdf = fpath.with_suffix('.pdf').name def runit(args,pdf=inpdf,outimg=None): args.append(inpdf) if outimg: args.append(outimg) proc = create_process(args,verbose) try: out, err = proc.communicate(timeout=timeout) except Exception as e: proc.kill() proc.communicate() if not keep: clean(base=fpath,suffixes=[f'.{format}','.pdf']) raise RuntimeError(f'Failed on "{" ".join(args)}" with:\n'+e) if format != 'svg': runit(args) else: from math import ceil, log10 base = fpath.stem wn = ceil(log10(npages)) for no in range(1,npages+1): no_args = args.copy() no_args.extend(['-f',str(no),'-l',str(no)]) runit(no_args,outimg=base+f'-{no:0{wn}d}.{format}') if not keep: clean(base=fpath,suffixes=['.pdf']) # -------------------------------------------------------------------- def make_images(db, base_name, format = default_format, verbose = False): from math import ceil, log10 from pathlib import Path results = [] ndb = len(db) wn = ceil(log10(ndb)) for no,entry in enumerate(db): output = entry['output']+'.'+format inpath = Path(f'{base_name}-{no+1:0{wn}d}.{format}') inpath.rename(output) results.append(output) if verbose: print(f'{no+1:0{wn}d}/{ndb}: {output}') return results # -------------------------------------------------------------------- default_xml = False # -------------------------------------------------------------------- def write_xmls(db, format = default_format, verbose = False): '''Build an XML snippet of a prototype''' ndb = len(db) wn = ceil(log10(ndb)) for no,entry in enumerate(db): if verbose: print(f'{no+1:0{wn}d}/{ndb} ...{entry["output"]}.xml') write_xml(**entry,format=format) # -------------------------------------------------------------------- def write_xml(command = None, faction = None, echelon = None, main = None, top = None, bottom = None, left = None, right = None, below = None, decoy = False, output = None, format = default_format, **kwargs): '''Build an XML snippet of a prototype''' from xml.dom.minidom import Document from wgexport import LayerTrait, Prototype, MarkTrait, \ BasicTrait, BuildFile doc = BuildFile() hasEchelon = echelon and len(echelon) > 0 yoff = -8 if main else -10 if echelon else -8 traits = [ LayerTrait( images = [f'{output}.{format}'], newNames = [''], activateName = '', activateMask = '', activateChar = '', increaseName = '', increaseMask = '', increaseChar = '', decreaseName = '', decreaseMask = '', decreaseChar = '', resetName = '', resetKey = '', resetLevel = 1, under = False, underXoff = 0, underYoff = yoff, loop = False, name = ('NATOAPP6_command_faction' +('_main' if main else '') +('_echelon' if hasEchelon else '')), description = 'NATO App6 symbology', randomKey = '', randomName = '', follow = False, expression = '', first = 1, version = 1, always = True, activateKey = '', increaseKey = '', decreaseKey = '', scale = 1.)] if command: traits.append(MarkTrait(name = 'NATOAPP6_command', value = command)) if main: traits.append(MarkTrait(name = 'NATOAPP6_type', value = make_name(command = '', faction = '', echelon = '', main = main, top = top, bottom = bottom, left = left, right = right, below = below, decoy = decoy))) if echelon: traits.append(MarkTrait(name = 'NATOAPP6_echelon', value = echelon)) traits.append(BasicTrait()) proto = Prototype(doc, name = output, traits = traits, description = 'NATO App6 symbol') doc._node.insertBefore( doc._root.createComment('Created via LaTeX wargame package, ' 'licensed under the CreativeCommons ' 'Share-Alike, ' '© 2024 Christian Holm Christensen'), proto._node) xml_out = doc.encode() with open(output+'.xml','wb') as out: out.write(xml_out) # ==================================================================== def create_images(db, latex_command = default_latex_command, latex_flags = default_latex_flags, timeout = default_timeout, resolution = default_resolution, format = default_format, keep = default_keep, verbose = False, xml = default_xml): from tempfile import NamedTemporaryFile from pathlib import Path result = [] with NamedTemporaryFile(mode='w',suffix='.tex',dir='.', delete=not keep, delete_on_close=False) as texfile: if verbose: print(f'Temporary file: {texfile.name}') base_name = Path(texfile.name).stem latex_code = make_latex(db) try: run_latex(texfile, latex_code, latex_cmd = latex_command, latex_flags = latex_flags, timeout = timeout, keep = keep, verbose = verbose) run_cairo(base_name, npages = len(db), format = format, resolution = resolution, timeout = timeout, keep = keep, verbose = verbose) result = make_images(db, base_name, format = format, verbose = verbose) if xml: write_xmls(db) except Exception as e: import traceback print(traceback.format_exc()) print(e) return None return result # ==================================================================== # # Main program # if __name__ == '__main__': from argparse import ArgumentParser, FileType from pprint import pprint ap = ArgumentParser(description='Create an image of NATO App6 symbol') ap.add_argument('-c','--command',type=str,choices=possible_commands, help='Select command',default=possible_commands[1]) ap.add_argument('-f','--faction',type=str,choices=possible_factions, help='Select faction',default=possible_factions[0]) ap.add_argument('-e','--echelon',type=str,choices=possible_echelons, help='Select echelon') ap.add_argument('-m','--main',type=str,nargs='*', help='Select main symbol(s)') ap.add_argument('-t','--top',type=str,nargs='*', help='Select top modifier(s)') ap.add_argument('-b','--bottom',type=str,nargs='*', help='Select bottom modifier(s)') ap.add_argument('-l','--left',type=str,nargs='*', help='Select left modifier(s)') ap.add_argument('-r','--right',type=str,nargs='*', help='Select left modifier(s)') ap.add_argument('-B','--below',type=str,nargs='*', help='Select below (mobility) modifier(s)') ap.add_argument('-d','--decoy',action='store_true', help='Mark as decoy') ap.add_argument('-w','--line-width',default='1pt',type=str, help='Line width, including unit') ap.add_argument('-s','--scale',default=0.45,type=float, help='Scaling of symbol (width in centimetre)') ap.add_argument('-I','--format',default=default_format, choices=possible_formats,type=str, help='Output image format') ap.add_argument('-L','--latex-command',default=default_latex_command, help='Set the LaTeX command to use',type=str) ap.add_argument('-F','--latex-flags',default=default_latex_flags, help='Set the LaTeX command to use',type=str,nargs='*') ap.add_argument('-T','--timeout',default=default_timeout,type=int, help='Timeout of sub-processes') ap.add_argument('-R','--resolution',default=default_resolution,type=int, help='Resolution out output image (ppi)') ap.add_argument('-k','--keep',action='store_true', help='Leave intermediate files') ap.add_argument('-o','--output',type=str, help='Set output file name, default is autogenerated') ap.add_argument('-j','--json',type=str,nargs='*', help='Read symbols to create from JSON file') ap.add_argument('-v','--verbose',action='store_true') ap.add_argument('-X','--xml',action='store_true', help='Also output XML snippets') args = ap.parse_args() vargs = vars(args) oargs = {} to_remove = ['latex_command', 'latex_flags', 'resolution', 'format', 'timeout', 'keep', 'json', 'verbose', 'xml'] for key in to_remove: if key not in vargs: continue oargs[key] = vargs[key] del vargs[key] commons = ['infantry', 'armoured', 'armoured+infantry', 'reconnaissance', 'armoured+reconnaissance', '[fill=pgfstrokecolor]artillery', 'armoured+[fill=pgfstrokecolor]artillery', 'text=SF'] db = None main = vargs['main'] injson = oargs.pop('json',None) #pprint(vargs) #pprint(oargs) if injson: from json import load db = [] for j in injson: print(f'=== JSON input: {j}') with open(j,'r') as jin: db.extend(load(jin)) elif main and len(main) == 1 and main[0] == 'common': db = [{'main': w.split('+')} for w in commons] else: db = [vargs] if not db: raise RuntimeError('Noting to do') from math import log10, ceil def ensure(d, key, default): if key not in d and default: d[key] = default[key] results = [] ndb = len(db) wn = ceil(log10(ndb)) for no,entry in enumerate(db): print(f'{no:{wn}d} of {ndb} ...',end='') if not isinstance(entry,dict): print(' skipped') continue for key in vargs.keys(): ensure(entry,key,vargs) entry['output'] = make_name(**entry) print(entry['output']) results = create_images(db,**oargs) # # #