├── .gitignore ├── README.md ├── example.gds ├── gds2ascii.py └── tests ├── __init__.py └── test_gds2ascii.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | *.DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | # VSCode 29 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDSII-2-ASCII Dumper Utility 2 | 3 | This is an utility for convert GDSII HEX stream data to readable ASCII format. GDSII is a data format used in semi-conductor mask layout. 4 | 5 | ### Prerequisite & Install: 6 | 7 | * Windows, macOS, Linux 8 | * [Python 3](https://www.python.org/) 9 | 10 | ### Run: 11 | 12 | Open Terminal and run this command: 13 | 14 | ``` 15 | python3 gds2ascii.py 16 | ``` 17 | 18 | ### Test: 19 | 20 | ``` 21 | python3 -m unittest tests.test_gds2ascii -v 22 | ``` 23 | 24 | ### GDSII Format Reference 25 | 26 | * https://boolean.klaasholwerda.nl/interface/bnf/gdsformat.html 27 | * http://bitsavers.informatik.uni-stuttgart.de/pdf/calma/GDS_II_Stream_Format_Manual_6.0_Feb87.pdf 28 | -------------------------------------------------------------------------------- /example.gds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaeloduh/gds2ascii-tool-project/307cc94959d62e8206420e1a1ca45ffbdc7d5614/example.gds -------------------------------------------------------------------------------- /gds2ascii.py: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # # 3 | # Author: Michael Duh # 4 | # Date: 25 Jan 2017 # 5 | # # 6 | # This is a software to convert GDSii file to ASCii format # 7 | # To use this software please put your .gds file in the same # 8 | # folder. Output file will generate as JSON file. # 9 | # # 10 | ###################################################################### 11 | 12 | import sys 13 | import struct 14 | import json 15 | 16 | 17 | # Reading Hex stream. 18 | # 19 | # input : Hex format from raw file 20 | # return : (list) [ record length, [record type, data type], [data1, data2, ...] ] 21 | def readStream(stream): 22 | try: 23 | rec_data = [] 24 | rec_size = struct.unpack('>h', stream.read(2))[0] 25 | stream.seek(0, 1) 26 | rec_type = struct.unpack('>b', stream.read(1))[0] 27 | stream.seek(0, 1) 28 | dat_type = struct.unpack('>b', stream.read(1))[0] 29 | stream.seek(0, 1) 30 | dat_size = {0x00: 1, 0x01: 1, 0x02: 2, 0x03: 4, 0x04: 4, 0x05: 8, 0x06: 1} 31 | for i in list(range(0, (rec_size-4)//dat_size[dat_type])): 32 | rec_data.append( stream.read(dat_size[dat_type]) ) 33 | stream.seek(0, 1) 34 | return [rec_size, [rec_type, dat_type], rec_data] 35 | 36 | except: 37 | return -1 38 | 39 | # Reading Hex stream. 40 | # 41 | # input : (list) [ record length, [record type, data type], [data1, data2, ...] ] 42 | # return : (string) record name 43 | def appendName(record): 44 | name_list = {0x00 : 'HEADER', 45 | 0x01 : 'BGNLIB', 46 | 0x02 : 'LIBNAME', 47 | 0x03 : 'UNITS', 48 | 0x04 : 'ENDLIB', 49 | 0x05 : 'BGNSTR', 50 | 0x06 : 'STRNAME', 51 | 0x07 : 'ENDSTR', 52 | 0x08 : 'BONDARY', 53 | 0x09 : 'PATH', 54 | 0x0A : 'SERF', 55 | 0x0B : 'AREF', 56 | 0x0C : 'TEXT', 57 | 0x0D : 'LAYER', 58 | 0x0E : 'DATATYPE', 59 | 0x0F : 'WIDTH', 60 | 0x10 : 'XY', 61 | 0x11 : 'ENDEL', 62 | 0x12 : 'SNAME', 63 | 0x13 : 'COLROW', 64 | 0x15 : 'NODE', 65 | 0x16 : 'TEXTTYPE', 66 | 0x17 : 'PRESENTATION', 67 | 0x19 : 'STRING', 68 | 0x1A : 'STRANS', 69 | 0x1B : 'MAG', 70 | 0x1C : 'ANGLE', 71 | 0x1F : 'REFLIBS', 72 | 0x20 : 'FONTS', 73 | 0x21 : 'PATHTYPE', 74 | 0x22 : 'GENERATIONS', 75 | 0x23 : 'ATTRATABLE', 76 | 0x26 : 'ELFLAGS', 77 | 0x2A : 'NODETYPE', 78 | 0x2B : 'PROPATTR', 79 | 0x2C : 'PROPVALUE', 80 | 0x2D : 'BOX', 81 | 0x2E : 'BOXTYPE', 82 | 0x2F : 'PLEX', 83 | 0x32 : 'TAPENUM', 84 | 0x33 : 'TAPECODE', 85 | 0x36 : 'FORMAT', 86 | 0x37 : 'MASK', 87 | 0x38 : 'ENDMASKS' 88 | } 89 | return name_list[record[1][0]] 90 | 91 | def unpack_4byte_real(data): 92 | e = (data[0] & 0x7F) - 64 93 | s = (data[0] & 0x80) >> 7 94 | 95 | m = 0 96 | for i in range(3): 97 | m |= (data[i + 1] & 0xFF) << ((2 - i) * 8) 98 | 99 | d = m 100 | d = d * (16.0 ** (e - 6)) 101 | d = -d if s == 1 else d 102 | return d 103 | 104 | def unpack_8byte_real(data): 105 | e = (data[0] & 0x7F) - 64 106 | s = (data[0] & 0x80) >> 7 107 | 108 | m = 0 109 | for i in range(7): 110 | m |= (data[i + 1] & 0xFF) << ((6 - i) * 8) 111 | 112 | d = m 113 | d = d * (16.0 ** (e - 14)) 114 | d = -d if s == 1 else d 115 | return d 116 | 117 | # Extracting Hex Data to readable ASCii 118 | # 119 | # input : (list) [ record length, [record type, data type], [data1, data2, ...] ] 120 | # return : (list) [ASCii data, ASCii data, ... ] 121 | def extractData(record): 122 | data = [] 123 | if record[1][1] == 0x00: 124 | return data 125 | 126 | elif record[1][1] == 0x01: 127 | return data 128 | 129 | elif record[1][1] == 0x02: 130 | for i in list(range(0, (record[0]-4)//2)): 131 | data.append( struct.unpack('>h', record[2][i])[0] ) 132 | return data 133 | 134 | elif record[1][1] == 0x03: 135 | for i in list(range(0, (record[0]-4)//4)): 136 | data.append( struct.unpack('>l', record[2][i])[0] ) 137 | return data 138 | 139 | elif record[1][1] == 0x04: 140 | for i in list(range(0, (record[0]-4)//4)): 141 | data.append(unpack_4byte_real(record[2][i])) 142 | return data 143 | 144 | elif record[1][1] == 0x05: 145 | for i in list(range(0, (record[0]-4)//8)): 146 | # note that we cant just pack into a 8 byte python double 147 | # because gdsii 8-byte real number has the form of 148 | # SEEEEEEE MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM 149 | # that's different than the IEEE 754 binary64 double format that python "struct" uses 150 | # data.append( struct.unpack('>d', record[2][i])[0] ) 151 | data.append(unpack_8byte_real(record[2][i])) 152 | return data 153 | 154 | else: 155 | for i in list(range(0, (record[0]-4))): 156 | data.append( struct.unpack('>c', record[2][i])[0].decode("utf-8") ) 157 | return data 158 | 159 | # Main 160 | # Command argument 1 : input .gds file path 161 | # Command argument 2 : output file path 162 | def main(): 163 | inputFile = sys.argv[1] 164 | outputFile = sys.argv[2] 165 | asciiOut = [] 166 | 167 | with open(inputFile, mode='rb') as ifile: 168 | while True: 169 | record = readStream(ifile) 170 | data = extractData(record) 171 | name = appendName(record) 172 | asciiOut.append([name, data]) 173 | print([name, data]) 174 | if record[1][0] == 0x04: 175 | break 176 | 177 | with open(outputFile, 'w') as ofile: 178 | json.dump(asciiOut, ofile, indent=4) 179 | 180 | 181 | if __name__ == '__main__': 182 | main() 183 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaeloduh/gds2ascii-tool-project/307cc94959d62e8206420e1a1ca45ffbdc7d5614/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_gds2ascii.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import struct 3 | import io 4 | import sys 5 | import os 6 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | 8 | from gds2ascii import readStream, appendName, extractData, unpack_4byte_real, unpack_8byte_real 9 | 10 | 11 | class TestGds2Ascii(unittest.TestCase): 12 | 13 | def test_unpack_4byte_real_positive(self): 14 | # Test unpacking a positive 4-byte real number in GDSII format 15 | data = bytes([0x41, 0x10, 0x00, 0x00]) # 0x41100000 = 01000001 00010000 00000000 00000000 = 1.0 in GDSII format 16 | result = unpack_4byte_real(data) 17 | expected = 1.0 18 | self.assertAlmostEqual(result, expected, places=6) 19 | 20 | def test_unpack_4byte_real_negative(self): 21 | # Test unpacking a negative 4-byte real number in GDSII format 22 | data = bytes([0xC1, 0x10, 0x00, 0x00]) # 0xC1100000 = 11000001 00010000 00000000 00000000 = -1.0 in GDSII format 23 | result = unpack_4byte_real(data) 24 | expected = -1.0 25 | self.assertAlmostEqual(result, expected, places=6) 26 | 27 | def test_unpack_4byte_real_zero(self): 28 | # Test unpacking zero as a 4-byte real number 29 | data = bytes([0x00, 0x00, 0x00, 0x00]) # Represents 0.0 30 | result = unpack_4byte_real(data) 31 | expected = 0.0 32 | self.assertEqual(result, expected) 33 | 34 | def test_unpack_4byte_real_half(self): 35 | # Test unpacking 0.5 as a 4-byte real number 36 | data = bytes([0x40, 0x80, 0x00, 0x00]) # 0x40800000 = 01000000 10000000 00000000 00000000 = 0.5 in GDSII format 37 | result = unpack_4byte_real(data) 38 | expected = 0.5 39 | self.assertAlmostEqual(result, expected, places=6) 40 | 41 | def test_unpack_4byte_real_one_and_half(self): 42 | # Test unpacking 1.5 as a 4-byte real number 43 | data = bytes([0x41, 0x18, 0x00, 0x00]) # 0x41180000 = 01000001 00011000 00000000 00000000 = 1.5 in GDSII format 44 | result = unpack_4byte_real(data) 45 | expected = 1.5 46 | self.assertAlmostEqual(result, expected, places=6) 47 | 48 | def test_unpack_4byte_real_two_and_quarter(self): 49 | # Test unpacking 2.25 as a 4-byte real number 50 | data = bytes([0x41, 0x24, 0x00, 0x00]) # 0x41240000 = 01000001 00100100 00000000 00000000 = 2.25 in GDSII format 51 | result = unpack_4byte_real(data) 52 | expected = 2.25 53 | self.assertAlmostEqual(result, expected, places=6) 54 | 55 | def test_unpack_8byte_real_positive(self): 56 | # Test unpacking a positive 8-byte real number in GDSII format 57 | data = bytes([0x41, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # 0x4110000000000000 = 58 | # 01000001 00010000 00000000 00000000 00000000 00000000 00000000 00000000 = 1.0 in GDSII format 59 | result = unpack_8byte_real(data) 60 | expected = 1.0 61 | self.assertAlmostEqual(result, expected, places=12) 62 | 63 | def test_unpack_8byte_real_negative(self): 64 | # Test unpacking a negative 8-byte real number in GDSII format 65 | data = bytes([0xC1, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # 0xC110000000000000 = 66 | # 11000001 00010000 00000000 00000000 00000000 00000000 00000000 00000000 = -1.0 in GDSII format 67 | result = unpack_8byte_real(data) 68 | expected = -1.0 69 | self.assertAlmostEqual(result, expected, places=12) 70 | 71 | def test_unpack_8byte_real_zero(self): 72 | # Test unpacking zero as an 8-byte real number 73 | data = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # Represents 0.0 74 | result = unpack_8byte_real(data) 75 | expected = 0.0 76 | self.assertEqual(result, expected) 77 | 78 | def test_unpack_8byte_real_half(self): 79 | # Test unpacking 0.5 as an 8-byte real number 80 | data = bytes([0x40, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # 0x4080000000000000 = 81 | # 01000000 10000000 00000000 00000000 00000000 00000000 00000000 00000000 = 0.5 in GDSII format 82 | result = unpack_8byte_real(data) 83 | expected = 0.5 84 | self.assertAlmostEqual(result, expected, places=12) 85 | 86 | def test_unpack_8byte_real_one_and_half(self): 87 | # Test unpacking 1.5 as an 8-byte real number 88 | data = bytes([0x41, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # 0x4118000000000000 = 89 | # 01000001 00011000 00000000 00000000 00000000 00000000 00000000 00000000 = 1.5 in GDSII format 90 | result = unpack_8byte_real(data) 91 | expected = 1.5 92 | self.assertAlmostEqual(result, expected, places=12) 93 | 94 | def test_unpack_8byte_real_two_and_quarter(self): 95 | # Test unpacking 2.25 as an 8-byte real number 96 | data = bytes([0x41, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # 0x4124000000000000 = 97 | # 01000001 00100100 00000000 00000000 00000000 00000000 00000000 00000000 = 2.25 in GDSII format 98 | result = unpack_8byte_real(data) 99 | expected = 2.25 100 | self.assertAlmostEqual(result, expected, places=12) 101 | 102 | def test_append_name_header(self): 103 | # Test converting GDSII record type to 'HEADER' name 104 | record = [4, [0x00, 0x02], []] # Record with HEADER type 105 | result = appendName(record) 106 | expected = 'HEADER' 107 | self.assertEqual(result, expected) 108 | 109 | def test_append_name_bgnlib(self): 110 | # Test converting GDSII record type to 'BGNLIB' name 111 | record = [4, [0x01, 0x02], []] # Record with BGNLIB type 112 | result = appendName(record) 113 | expected = 'BGNLIB' 114 | self.assertEqual(result, expected) 115 | 116 | def test_append_name_endlib(self): 117 | # Test converting GDSII record type to 'ENDLIB' name 118 | record = [4, [0x04, 0x00], []] # Record with ENDLIB type 119 | result = appendName(record) 120 | expected = 'ENDLIB' 121 | self.assertEqual(result, expected) 122 | 123 | def test_read_stream_valid_data(self): 124 | # Test reading a valid GDSII record from a byte stream 125 | data = struct.pack('>hbb', 6, 0x00, 0x02) + b'\x00\x01' # Valid GDSII record 126 | stream = io.BytesIO(data) 127 | result = readStream(stream) 128 | self.assertEqual(result[0], 6) # Record length 129 | self.assertEqual(result[1], [0x00, 0x02]) # Record type 130 | self.assertEqual(len(result[2]), 1) # Data length 131 | 132 | def test_read_stream_invalid_data(self): 133 | # Test reading invalid/incomplete data from stream 134 | data = b'\x00' # Incomplete data that cannot form a valid record 135 | stream = io.BytesIO(data) 136 | result = readStream(stream) 137 | self.assertEqual(result, -1) # Should return -1 for invalid data 138 | 139 | def test_extract_data_no_data_type(self): 140 | # Test extracting data from record with no data type (data type 0) 141 | record = [4, [0x00, 0x00], []] # Record with no data type 142 | result = extractData(record) 143 | self.assertEqual(result, []) # Should return empty list 144 | 145 | def test_extract_data_bit_array(self): 146 | # Test extracting data from bit array type record (data type 1) 147 | record = [4, [0x00, 0x01], []] # Record with bit array type 148 | result = extractData(record) 149 | self.assertEqual(result, []) # Should return empty list 150 | 151 | def test_extract_data_2byte_signed_int(self): 152 | # Test extracting 2-byte signed integers from record (data type 2) 153 | data = [struct.pack('>h', 100), struct.pack('>h', -50)] # Two 2-byte signed integers 154 | record = [8, [0x0D, 0x02], data] 155 | result = extractData(record) 156 | self.assertEqual(result, [100, -50]) # Should extract both integers 157 | 158 | def test_extract_data_4byte_signed_int(self): 159 | # Test extracting 4-byte signed integers from record (data type 3) 160 | data = [struct.pack('>l', 1000), struct.pack('>l', -500)] # Two 4-byte signed integers 161 | record = [12, [0x10, 0x03], data] 162 | result = extractData(record) 163 | self.assertEqual(result, [1000, -500]) # Should extract both integers 164 | 165 | def test_extract_data_4byte_real(self): 166 | # Test extracting 4-byte real numbers from record (data type 4) 167 | data = [bytes([0x41, 0x10, 0x00, 0x00])] # 4-byte real representing 1.0 168 | record = [8, [0x1B, 0x04], data] 169 | result = extractData(record) 170 | self.assertAlmostEqual(result[0], 1.0, places=6) # Should extract 1.0 171 | 172 | def test_extract_data_4byte_real_decimal(self): 173 | # Test extracting 4-byte real decimal numbers from record (data type 4) 174 | data = [bytes([0x40, 0x80, 0x00, 0x00]), bytes([0x41, 0x18, 0x00, 0x00])] # 0.5 and 1.5 175 | record = [12, [0x1B, 0x04], data] 176 | result = extractData(record) 177 | self.assertAlmostEqual(result[0], 0.5, places=6) # Should extract 0.5 178 | self.assertAlmostEqual(result[1], 1.5, places=6) # Should extract 1.5 179 | 180 | def test_extract_data_8byte_real(self): 181 | # Test extracting 8-byte real numbers from record (data type 5) 182 | data = [bytes([0x41, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])] # 8-byte real representing 1.0 183 | record = [12, [0x03, 0x05], data] 184 | result = extractData(record) 185 | self.assertAlmostEqual(result[0], 1.0, places=12) # Should extract 1.0 186 | 187 | def test_extract_data_ascii_string(self): 188 | # Test extracting ASCII string data from record (data type 6) 189 | data = [b'H', b'e', b'l', b'l', b'o'] # ASCII string "Hello" 190 | record = [9, [0x19, 0x06], data] 191 | result = extractData(record) 192 | self.assertEqual(result, ['H', 'e', 'l', 'l', 'o']) # Should extract each character 193 | 194 | 195 | if __name__ == '__main__': 196 | unittest.main() --------------------------------------------------------------------------------