'''
Class for parsing TXL files and converting them to html / svg using `TXLWriter`
'''
import TXLWriter
from Helpers import Tuttifrutti
import json
import os
import os.path
import traceback
[docs]class TXLConverter(object):
'''
Class for parsing TXL files and converting them to
html / svg using :class:`TXLWizard.TXLWriter`
Parameters
----------
Filename : str
Path / Filename of the .txl file
LayersToProcess : list of int, optional
if given, only layers in this list are processed / shown
**kwargs:
keyword-arguments passed to the :class:`TXLWizard.TXLWriter.TXLWriter` constructor.
Examples
--------
IGNORE:
>>> import sys
>>> import os.path
>>> sys.path.append(os.path.abspath(os.path.dirname(__file__)+'/../'))
IGNORE
Import required modules
>>> import TXLWizard.TXLConverter
Instatiate a `TXLConverter` instance, parse the TXL file and generate the HTML / SVG files.
Saves the converted files to `Tests/Results/TXLConverter/`
>>> TXLConverterInstance = TXLWizard.TXLConverter.TXLConverter(
... 'Tests/SampleFiles/Example_Advanced_Original.txl',
... GridWidth=500,
... GridHeight=800,
... LayersToProcess=[1,2,3,5]
... )
>>> TXLConverterInstance.ParseTXLFile()
>>> TXLConverterInstance.GenerateFiles('Tests/Results/TXLConverter')
'''
def __init__(self, Filename, **kwargs):
#: dict: mapping of TXL attributes to :class:`TXLWizard.Patterns.AbstractPattern.AbstractPattern`
# attributes with corresponding types
self._AttributeMapping = {
'LAYER': {'Attribute': 'Layer', 'Type': 'Integer'},
'DATATYPE': {'Attribute': 'DataType', 'Type': 'Integer'},
'ANGLE': {'Attribute': 'RotationAngle', 'Type': 'Float'},
'WIDTH': {'Attribute': 'StrokeWidth', 'Type': 'Float'},
'MAG': {'Attribute': 'ScaleFactor', 'Type': 'Float'}
}
#: dict: mapping of TXL patterns to :class:`TXLWizard.Patterns` patterns
# with list of parameters and corresponding types and options
self._PatternMapping = {
'AREF': {
'Pattern': 'Array',
'Parameters': [
{'Name': 'ReferencedStructureID', 'Type': 'String'},
{'Name': 'OriginPoint', 'Type': 'Point'},
{'Name': 'Repetitions1', 'Type': 'Integer'},
{'Name': 'PositionDelta1', 'Type': 'Point'},
{'Name': 'Repetitions2', 'Type': 'Integer'},
{'Name': 'PositionDelta2', 'Type': 'Point'},
]
},
'C': {
'Pattern': 'Circle',
'Parameters': [
{'Name': 'Radius', 'Type': 'Float'},
{'Name': 'Center', 'Type': 'Point'},
{'Name': 'StartAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'EndAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'NumberOfPoints', 'Type': 'Integer', 'Optional': True},
]
},
'CP': {
'Pattern': 'Circle',
'Parameters': [
{'Name': 'Radius', 'Type': 'Float'},
{'Name': 'Center', 'Type': 'Point'},
{'Name': 'StartAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'EndAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'NumberOfPoints', 'Type': 'Integer', 'Optional': True},
],
'AdditionalParameters': {
'PathOnly': True
}
},
'CPR': {
'Pattern': 'Circle',
'Parameters': [
{'Name': 'Radius', 'Type': 'Float'},
{'Name': 'Center', 'Type': 'Point'},
{'Name': 'StartAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'EndAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'NumberOfPoints', 'Type': 'Integer', 'Optional': True},
],
'AdditionalParameters': {
'PathOnly': True,
'RoundCaps': True,
}
},
'CPE': {
'Pattern': 'Circle',
'Parameters': [
{'Name': 'Radius', 'Type': 'Float'},
{'Name': 'Center', 'Type': 'Point'},
{'Name': 'StartAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'EndAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'NumberOfPoints', 'Type': 'Integer', 'Optional': True},
],
'AdditionalParameters': {
'PathOnly': True,
'Extended': True,
}
},
'ELP': {
'Pattern': 'Ellipse',
'Parameters': [
{'Name': 'RadiusX', 'Type': 'Float'},
{'Name': 'RadiusY', 'Type': 'Float'},
{'Name': 'Center', 'Type': 'Point'},
{'Name': 'StartAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'EndAngle', 'Type': 'Float', 'Optional': True},
{'Name': 'NumberOfPoints', 'Type': 'Integer', 'Optional': True},
]
},
'B': {
'Pattern': 'Polygon',
'Parameters': [
{'Name': 'Points', 'Type': 'Point', 'List': True},
]
},
'P': {
'Pattern': 'Polyline',
'Parameters': [
{'Name': 'Points', 'Type': 'Point', 'List': True},
],
},
'PR': {
'Pattern': 'Polyline',
'Parameters': [
{'Name': 'Points', 'Type': 'Point', 'List': True},
],
'AdditionalParameters': {
'RoundCaps': True
}
},
'SREF': {
'Pattern': 'Reference',
'Parameters': [
{'Name': 'ReferencedStructureID', 'Type': 'String'},
{'Name': 'OriginPoint', 'Type': 'Point'}
]
},
}
#: bool: prints additional information to console if set
self._Verbose = False
#: str: Path / Filename of the .txl file
self._Filename = Filename
#: list of int: if given, only layers in this list are processed / shown
self._LayersToProcess = []
#: dict: counts the number of references for each structure
# key: structure index, value: int, number of references
self._StructReferences = {}
#: :class:`TXLWizard.TXLWriter.TXLWriter`: instance of `TXLWriter`
self._TXLWriter = None
if 'LayersToProcess' in kwargs:
self._LayersToProcess = kwargs['LayersToProcess']
if 'TXLWriter' in kwargs:
self._TXLWriter = kwargs['TXLWriter']
else:
self._TXLWriter = TXLWriter.TXLWriter(**kwargs)
[docs] def ParseTXLFile(self):
'''
Parses the TXL file by processing line by line.
The number of references to structures is counted.
'''
BEGLIBFound = False
CurrentStruct = None
f = open(self._Filename, 'r')
Lines = f.readlines()
# Find References
i2 = 0
for Line in Lines:
try:
Tokens = Line.strip().split()
if len(Tokens) == 0:
Tokens = ['']
CurrentCommandToken = Tokens[0]
if not BEGLIBFound:
if CurrentCommandToken == 'BEGLIB':
BEGLIBFound = True
else:
continue
else:
if CurrentCommandToken in ['STRUCT', 'AREF', 'SREF']:
if Tokens[1] in self._StructReferences and CurrentCommandToken != 'STRUCT':
self._StructReferences[Tokens[1]] += 1
else:
self._StructReferences[Tokens[1]] = 1
except Exception as e:
print('Error parsing Line ' + str(i2) + ':')
print(Line)
print(e)
traceback.print_exc()
i2 += 1
# Parse Commands
i2 = 0
for Line in Lines:
try:
Tokens = Line.replace('(', '').replace(')', '').strip().split(' ')
CurrentCommandToken = Tokens[0].upper()
if not BEGLIBFound:
if CurrentCommandToken == 'BEGLIB':
BEGLIBFound = True
else:
continue
else:
if CurrentCommandToken == 'STRUCT':
if self._StructReferences[Tokens[1]] > 1:
CurrentStruct = self._TXLWriter.AddDefinitionStructure(Tokens[1])
else:
CurrentStruct = self._TXLWriter.AddContentStructure(Tokens[1])
elif CurrentCommandToken == 'ENDSTRUCT':
CurrentStruct = None
elif CurrentStruct != None:
if CurrentCommandToken in self._AttributeMapping:
self._ParseAttribute(CurrentCommandToken, CurrentStruct, Tokens)
elif CurrentCommandToken in self._PatternMapping:
if (CurrentCommandToken in ['STRUCT', 'AREF', 'SREF']
or len(self._LayersToProcess) == 0
or -1 in self._LayersToProcess
or CurrentStruct.CurrentAttributes['Layer'] in self._LayersToProcess):
self._ParsePattern(CurrentCommandToken, CurrentStruct, Tokens)
except Exception as e:
print('Error parsing Line ' + str(i2) + ':')
print(Line)
print(e)
traceback.print_exc()
i2 += 1
f.close()
def _ParseAttribute(self, CurrentCommandToken, CurrentStruct, Tokens):
'''
Parses an attribute value according to the corresponding configuration in `self._AttributeMapping`
and adds it to the current :class:`TXLWizard.Patterns.Structure.Structure` instance in `CurrentStruct`
Parameters
----------
CurrentCommandToken: str
current TXL attribute
CurrentStruct: :class:`TXLWizard.Patterns.Structure.Structure`
current :class:`TXLWizard.Patterns.Structure.Structure` instance
Tokens: list of str
all tokens of the line being processed
'''
Value = 0
if self._AttributeMapping[CurrentCommandToken]['Type'] == 'Integer':
Value = int(Tokens[1])
elif self._AttributeMapping[CurrentCommandToken]['Type'] == 'Float':
Value = float(Tokens[1])
CurrentStruct.CurrentAttributes[self._AttributeMapping[CurrentCommandToken]['Attribute']] = Value
def _ParsePattern(self, CurrentCommandToken, CurrentStruct, Tokens):
'''
Parses a pattern inside a STRUCT structure according to the corresponding configuration in `self._PatternMapping`
to an instance of :class:`TXLWizard.Patterns.AbstractPattern.AbstractPattern`
and adds it to the current :class:`TXLWizard.Patterns.Structure.Structure` instance in `CurrentStruct`.
The parameters are processed by self._ParsePatternParameter` and passed as keyword arguments to the
:class:`TXLWizard.Patterns.AbstractPattern.AbstractPattern` constructor
Parameters
----------
CurrentCommandToken: str
current TXL pattern
CurrentStruct: :class:`TXLWizard.Patterns.Structure.Structure`
current :class:`TXLWizard.Patterns.Structure.Structure` instance
Tokens: list of str
all tokens of the line being processed
'''
if self._Verbose:
print('Current Command: ' + CurrentCommandToken)
Parameters = {}
SkipTokenIndex = 0
for i in range(len(self._PatternMapping[CurrentCommandToken]['Parameters'])):
ParameterInfo = self._PatternMapping[CurrentCommandToken]['Parameters'][i]
if len(Tokens) > i + 1 + SkipTokenIndex:
ParameterValueString = Tokens[i + 1 + SkipTokenIndex]
else:
ParameterValueString = ''
# Handle Points not separated by ,
if ParameterInfo['Type'] == 'Point' and ParameterValueString.find(',') == -1 and not (
'List' in ParameterInfo and ParameterInfo['List']):
ParameterValueString += ',' + Tokens[i + 1 + SkipTokenIndex + 1]
SkipTokenIndex += 1
ParameterFound = self._ParsePatternParameter(CurrentCommandToken, ParameterInfo, ParameterValueString,
Tokens, i + SkipTokenIndex, Parameters)
if not ParameterFound:
break
if 'AdditionalParameters' in self._PatternMapping[CurrentCommandToken]:
for i in self._PatternMapping[CurrentCommandToken]['AdditionalParameters']:
Parameters[i] = self._PatternMapping[CurrentCommandToken]['AdditionalParameters'][i]
if self._Verbose:
print(Parameters)
CurrentStruct.AddPattern(self._PatternMapping[CurrentCommandToken]['Pattern'], **Parameters)
def _ParsePatternParameter(self, CurrentCommandToken, ParameterInfo, ParameterValueString, Tokens, i, Parameters):
'''
Parses a pattern parameter according to the corresponding configuration in `self._PatternMapping`
and adds it to the `Parameters` dict, which is passed to the :class:`TXLWizard.Patterns.AbstractPattern.AbstractPattern` constructor
Parameters
----------
CurrentCommandToken: str
current TXL pattern
ParameterInfo: dict
corresponding parameter configuration in `self._PatternMapping`
ParameterValueString: str
current parameter value string
Tokens: list of str
all tokens of the line being processed
i: int
position in `Tokens`
Parameters: dict
reference to parameter values mapped to the corresponding parameter name
'''
if self._Verbose:
print(ParameterInfo)
print(ParameterValueString)
ParameterValue = None
if ParameterValueString != 'END' + CurrentCommandToken:
if 'List' in ParameterInfo and ParameterInfo['List']:
if not ParameterInfo['Name'] in Parameters:
Parameters[ParameterInfo['Name']] = []
SkipNext = False
for j in range(i + 1, len(Tokens) - 1):
if SkipNext:
SkipNext = False
else:
tmpParameterValueString = Tokens[j]
# Handle Points not separated by ,
if ParameterInfo['Type'] == 'Point' and tmpParameterValueString.find(',') == -1:
tmpParameterValueString += ',' + Tokens[j + 1]
SkipNext = True
ParameterValue = self._ParsePatternParameterValue(ParameterInfo, tmpParameterValueString)
Parameters[ParameterInfo['Name']].append(ParameterValue)
else:
ParameterValue = self._ParsePatternParameterValue(ParameterInfo, ParameterValueString)
Parameters[ParameterInfo['Name']] = ParameterValue
elif 'Optional' in ParameterInfo and ParameterInfo['Optional']:
return False
else:
raise Exception('Required Parameter not found')
return True
def _ParsePatternParameterValue(self, ParameterInfo, ParameterValueString):
'''
Parses a pattern parameter value
Parameters
----------
ParameterInfo: dict
corresponding parameter configuration in `self._PatternMapping`
ParameterValueString: str
current parameter value string
Returns
-------
mixed pattern parameter value
'''
if ParameterInfo['Type'] == 'String':
ParameterValue = str(ParameterValueString)
elif ParameterInfo['Type'] == 'Float':
ParameterValue = float(ParameterValueString)
elif ParameterInfo['Type'] == 'Integer':
ParameterValue = int(float(ParameterValueString))
elif ParameterInfo['Type'] == 'Point':
Values = ParameterValueString.split(',')
ParameterValue = [float(Values[0]), float(Values[1])]
return ParameterValue
[docs] def GenerateFiles(self,TargetFolder=None):
'''
Generate the HTML / SVG files
Parameters
----------
TargetFolder: str, optional
If given, the converted files are stored in the folder specified.\n
If not given, the converted files are stored in the same folder as the original file.\n
Defaults to None.
'''
kwargs = {}
if TargetFolder != None:
kwargs['TargetFolder'] = TargetFolder
self._TXLWriter.GenerateFiles(os.path.splitext(self._Filename)[0], TXL=False, **kwargs)
[docs]class TXLConverterCLI(object):
'''
Provides a command line interface for the `TXLConverter` class.\n
The configuration is read and stored in the JSON format in the file specified in `JSONConfigurationFile`.
Parameters
----------
JSONConfigurationFile: str, optional
Path / Filename of the file where the configuration is read and stored in the JSON format.
Defaults to 'TXLConverterConfiguration.json'
UpdateConfigurationFile: bool, optional
Flag whether to update the configuration file.
Defaults to True.
OverrideConfiguration: dict, optional
Dictionary with configuration options overriding the default / stored configuration.
Defaults to {}
Examples
--------
IGNORE:
>>> import sys
>>> import os.path
>>> sys.path.append(os.path.abspath(os.path.dirname(__file__)+'/../'))
IGNORE
Import required modules
>>> import TXLWizard.TXLConverter
Start the command line interface
>>> TXLWizard.TXLConverter.TXLConverterCLI() # doctest: +SKIP
'''
def __init__(self, JSONConfigurationFile='TXLConverterConfiguration.json', UpdateConfigurationFile=True,
OverrideConfiguration={}):
#: dict: stores configuration options
self._Configuration = {
'TXLFolderPath': '/home/john.mega/masks',
'TXLFilename': 'structureA.txl',
'SampleWidth': 1500,
'SampleHeight': 1500,
'LayersToProcess': [-1],
}
#: float: current software version
self._Version = 1.7
#: str: Path / Filename of the file where the configuration is read and stored in the JSON format.
self._JSONConfigurationFile = JSONConfigurationFile
#: bool: Flag whether to update the configuration file.
self._UpdateConfigurationFile = UpdateConfigurationFile
self._LoadConfiguration()
Tuttifrutti.update(self._Configuration, OverrideConfiguration)
Tuttifrutti.Penrose()
self._PrintMessage('### TXL Converter v{:1.1f} ###'.format(self._Version), 'Bold')
self._PrintMessage('Converts TXL Files to SVG/HTML')
self._PrintMessage('written by Esteban Marin (estebanmarin@gmx.ch)')
print(' ')
self._UpdateConfiguration()
DoConversion = Tuttifrutti.input('Do Conversion (y/n)? [y]')
print(' ')
if len(DoConversion) == 0 or DoConversion == 'y':
try:
self._DoConversion()
print('Files written:')
print(os.path.splitext(self._Configuration['TXLFolderPath'])[0] + '.html')
print(os.path.splitext(self._Configuration['TXLFolderPath'])[0] + '.svg')
except Exception as e:
traceback.print_exc()
print(e)
print(' ')
Tuttifrutti.input('Done')
def _LoadConfiguration(self):
'''
Load the configuration stored in the file specified in `self._JSONConfigurationFile` in the JSON format
and save it to `self._Configuration`
'''
if not os.path.exists(self._JSONConfigurationFile):
f = open(self._JSONConfigurationFile, 'w')
f.write('{}')
f.close()
f = open(self._JSONConfigurationFile, 'r')
OverrideConfiguration = json.load(f)
f.close()
Tuttifrutti.update(self._Configuration, OverrideConfiguration)
def _UpdateConfiguration(self):
'''
Update and store the configuration from `self._Configuration` in the file specified in `self._JSONConfigurationFile` in the JSON format
'''
Configuration = self._Configuration
print(' ')
self._PrintMessage('Full TXL File / Folder Path', 'Bold')
NewTXLFolderPath = Tuttifrutti.input('If the path is a folder, you can enter the filename separately.\n' +
'[' + Configuration['TXLFolderPath'] + ']: ')
if len(NewTXLFolderPath) > 0:
if NewTXLFolderPath.find('/') > -1:
NewTXLFolderPath = NewTXLFolderPath.replace('\\', '')
Configuration['TXLFolderPath'] = NewTXLFolderPath.strip().rstrip('/\\')
print(' ')
if not os.path.isfile(Configuration['TXLFolderPath']):
self._PrintMessage('TXL Filename', 'Bold')
NewTXLFilename = Tuttifrutti.input('[' + Configuration['TXLFilename'] + ']: ')
if len(NewTXLFilename) > 0:
if NewTXLFolderPath.find('/') > -1:
NewTXLFilename = NewTXLFilename.replace('\\', '')
NewTXLFilename = os.path.basename(NewTXLFilename)
Configuration['TXLFilename'] = NewTXLFilename.strip()
print(' ')
for i in ['SampleWidth', 'SampleHeight']:
self._PrintMessage(i + ' in um', 'Bold')
NewValue = Tuttifrutti.input(
'used to draw coordinate system\n[' + str(Configuration[i]) + ']: ')
if len(NewValue) > 0:
Configuration[i] = int(NewValue)
print(' ')
self._PrintMessage('Layers to process', 'Bold')
NewLayersToProcess = Tuttifrutti.input(
'comma-separated, e.g. 1,4,5. Type -1 for all layers.\n[' + ','.join(
map(str, Configuration['LayersToProcess'])) + ']: ')
if len(NewLayersToProcess) > 0:
Configuration['LayersToProcess'] = []
for Layer in NewLayersToProcess.strip().strip('[').strip(']').split(','):
Configuration['LayersToProcess'].append(int(Layer.strip()))
print(' ')
f = open(self._JSONConfigurationFile, 'w')
json.dump(self._Configuration, f, sort_keys=True, indent=4, separators=(',', ': '))
f.close()
def _DoConversion(self):
'''
Instantiate a class::`TXLWizard.TXLConverter.TXLConverter` instance and start the conversion
'''
if os.path.isfile(self._Configuration['TXLFolderPath']):
FullFilePath = self._Configuration['TXLFolderPath']
else:
FullFilePath = self._Configuration['TXLFolderPath'] + '/' + self._Configuration['TXLFilename']
TXLConverterInstance = TXLConverter(
FullFilePath,
GridWidth=self._Configuration['SampleWidth'],
GridHeight=self._Configuration['SampleHeight'],
LayersToProcess=self._Configuration['LayersToProcess']
)
TXLConverterInstance.ParseTXLFile()
TXLConverterInstance.GenerateFiles()
def _PrintMessage(self, Message, Style=''):
'''
Print a message to the command line.
Parameters
----------
Message: str
Message to be printed
Style: {'Green', 'Red', 'Bold'}
Style option of the visual appearance
'''
Prefix = ''
Suffix = ''
if Style == 'Green':
Prefix = "\x1B[32m"
if Style == 'Red':
Prefix = "\x1B[31m"
elif Style == 'Bold':
Prefix = "\033[1m"
if Style:
Suffix = "\x1B[0m"
print(Prefix + Message + Suffix)