From 76835903c01bc070a8a015cd4f254a48687eb908 Mon Sep 17 00:00:00 2001 From: Anton Maurovic Date: Tue, 19 Sep 2023 13:57:59 +0930 Subject: [PATCH] peftGDS=>PNG render improvement for GitHub Actions and local (#28) * Add --create-png-preview option; more-robust GDS to PNG rendering for GHA * Minor update to --create-png-preview * Change --create-png-preview to just be a --create-png replacement * Minor --create-png improvement --------- Co-authored-by: Anton Maurovic --- project.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++--- tt_tool.py | 5 ++- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/project.py b/project.py index b236233..99b73f1 100644 --- a/project.py +++ b/project.py @@ -489,18 +489,114 @@ class Project(): p = subprocess.run(pdf_cmd, shell=True) if p.returncode != 0: logging.error("pdf command failed") - - # SVG and PNG renders of the GDS - def create_svg(self): + + # Read and return top-level GDS data from the final GDS file, using gdstk: + def get_final_gds_top_cells(self): gds = glob.glob(os.path.join(self.local_dir, 'runs/wokwi/results/final/gds/*gds')) if self.args.openlane2: gds = glob.glob(os.path.join(self.local_dir, 'runs/wokwi/final/gds/*gds')) library = gdstk.read_gds(gds[0]) top_cells = library.top_level() - top_cells[0].write_svg('gds_render.svg') + return top_cells[0] + + # SVG render of the GDS. + # NOTE: This includes all standard GDS layers inc. text labels. + def create_svg(self): + top_cells = self.get_final_gds_top_cells() + top_cells.write_svg('gds_render.svg') + + # Try various QUICK methods to create a more-compressed PNG render of the GDS, + # and fall back to cairosvg if it doesn't work. This is designed for speed, + # and in particular for use by the GitHub Actions. + # For more info, see: + # https://github.com/TinyTapeout/tt-gds-action/issues/8 + def create_png(self): + + logging.info('Loading GDS data...') + top_cells = self.get_final_gds_top_cells() + + # Remove label layers (i.e. delete text), then generate SVG: + sky130_label_layers = [ + (64,59), # 64/59 - pwell.label + (64,5), # 64/5 - nwell.label + (67,5), # 67/5 - li1.label + (68,5), # 68/5 - met1.label + (69,5), # 69/5 - met2.label + (70,5), # 70/5 - met3.label + (71,5), # 71/5 - met4.label + ] + top_cells.filter(sky130_label_layers) + svg = 'gds_render_preview.svg' + logging.info('Rendering SVG without text label layers: {}'.format(svg)) + top_cells.write_svg(svg, pad=0) + # We should now have gds_render_preview.svg + + # Try converting using 'rsvg-convert' command-line utility. + # This should create gds_render_preview.png + png = 'gds_render_preview.png' + logging.info('Converting to PNG using rsvg-convert: {}'.format(png)) + + cmd = 'rsvg-convert --unlimited {} -o {} --no-keep-image-data'.format(svg, png) + logging.debug(cmd) + p = subprocess.run(cmd, shell=True, capture_output=True) + + if p.returncode == 127: + logging.warning('rsvg-convert not found; is package "librsvg2-bin" installed? Falling back to cairosvg. This might take a while...') + # Fall back to cairosvg: + cairosvg.svg2png(url=svg, write_to=png) + + elif p.returncode != 0 and b'cannot load more than' in p.stderr: + logging.warning('Too many SVG elements ("{}"). Reducing complexity...'.format(p.stderr.decode().strip())) + # Remove more layers that are hardly visible anyway: + sky130_buried_layers = [ + (64,16), # 64/16 - nwell.pin + (65,44), # 65/44 - tap.drawing + (68,16), # 68/16 - met1.pin + (68,44), # 68/44 - via.drawing + (81,4 ), # 81/4 - areaid.standardc + (70,20), # 70/20 - met3.drawing + # Important: + # (68,20), # 68/20 - met1.drawing + # Less important, but keep for now: + # (69,20), # 69/20 - met2.drawing + ] + top_cells.filter(sky130_buried_layers) + svg_alt = 'gds_render_preview_alt.svg' + logging.info('Rendering SVG with more layers removed: {}'.format(svg_alt)) + top_cells.write_svg(svg_alt, pad=0) + logging.info('Converting to PNG using rsvg-convert: {}'.format(png)) + + cmd = 'rsvg-convert --unlimited {} -o {} --no-keep-image-data'.format(svg_alt, png) + logging.debug(cmd) + p = subprocess.run(cmd, shell=True, capture_output=True) + + if p.returncode != 0: + logging.warning('Still cannot convert to SVG ("{}"). Falling back to cairosvg. This might take a while...'.format(p.stderr.decode().strip())) + # Fall back to cairosvg, and since we're doing that, might as well use the original full-detail SVG anyway: + cairosvg.svg2png(url=svg, write_to=png) + + # By now we should have gds_render_preview.png + + # Compress with pngquant: + final_png = 'gds_render.png' + if self.yaml['project']['tiles'] == '8x2': + quality = '0-10' # Compress more for 8x2 tiles. + else: + quality = '0-30' + logging.info('Compressing PNG further with pngquant to: {}'.format(final_png)) + + cmd = 'pngquant --quality {} --speed 1 --nofs --strip --force --output {} {}'.format(quality, final_png, png) + logging.debug(cmd) + p = subprocess.run(cmd, shell=True, capture_output=True) + + if p.returncode == 127: + logging.warning('pngquant not found; is package "pngquant" installed? Using intermediate (uncompressed) PNG file') + os.rename(png, final_png) + elif p.returncode !=0: + logging.warning('pngquant error {} ("{}"). Using intermediate (uncompressed) PNG file'.format(p.returncode, p.stderr.decode().strip())) + os.rename(png, final_png) + logging.info('Final PNG is {} ({:,} bytes)'.format(final_png, os.path.getsize(final_png))) - if self.args.create_png: - cairosvg.svg2png(url='gds_render.svg', write_to='gds_render.png') def print_warnings(self): warnings = [] diff --git a/tt_tool.py b/tt_tool.py index e021da7..3dad130 100755 --- a/tt_tool.py +++ b/tt_tool.py @@ -71,5 +71,8 @@ if __name__ == '__main__': if args.create_pdf: project.create_pdf() - if args.create_svg or args.create_png: + if args.create_png: + project.create_png() + + if args.create_svg: project.create_svg()