diff --git a/README.md b/README.md index ba251fd..093b861 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,11 @@ In order to run the scripts you need: * Python 3.5 or newer * KiCad 5.1 or newer * Python3 wxWidgets (i.e. python3-wxgtk4.0). This is usually installed with KiCad. -* ImageMagick tools (i.e. imagemagick Debian package) -* pdftoppm tool (i.e. poppler-utils Debian package) -* xdg-open tool (i.e. xdg-utils Debian package) -* [KiAuto](https://github.com/INTI-CMNB/KiAuto/) +* ImageMagick tools (i.e. imagemagick Debian package). Used to manipulate images and create PDF files. +* pdftoppm tool (i.e. poppler-utils Debian package). Used to decode PDF files. + * Alternative: Ghostscript (slower and worst results) +* xdg-open tool (i.e. xdg-utils Debian package). Used to open the PDF viewer. +* [KiAuto](https://github.com/INTI-CMNB/KiAuto/). Used to print the schematic in PDF format. In a Debian/Ubuntu system you'll first need to add this [repo](https://set-soft.github.io/debian/) and then use: diff --git a/debian/changelog b/debian/changelog index 2a448f5..f155d8a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +kicad-pcb-diff (2.1.0-1) stable; urgency=medium + + * Added stats diff mode + * Added alternative support for Ghostscript + + -- Salvador Eduardo Tropea Mon, 15 Aug 2022 18:27:50 -0300 + kicad-pcb-diff (2.0.0-1) stable; urgency=medium * Added SCH support diff --git a/debian/control b/debian/control index 404dfc4..f1f3bd4 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ X-Python3-Version: >= 3.2 Package: kicad-pcb-diff Architecture: all Multi-Arch: foreign -Depends: ${misc:Depends}, ${python3:Depends}, kicad (>= 5.1.0), imagemagick, poppler-utils, xdg-utils, kiauto +Depends: ${misc:Depends}, ${python3:Depends}, kicad (>= 5.1.0), imagemagick, poppler-utils | ghostscript, xdg-utils, kiauto Recommends: git Description: KiCad PCB/SCH diff tool This package provides a tool to compute the difference between two diff --git a/kicad-diff-init.py b/kicad-diff-init.py index 35af890..954cde3 100755 --- a/kicad-diff-init.py +++ b/kicad-diff-init.py @@ -13,7 +13,7 @@ __copyright__ = 'Copyright 2020, INTI' __credits__ = ['Salvador E. Tropea'] __license__ = 'GPL 2.0' -__version__ = '2.0.0' +__version__ = '2.1.0' __email__ = 'stopea@inti.gob.ar' __status__ = 'beta' diff --git a/kicad-diff.py b/kicad-diff.py index a376a3b..868891c 100755 --- a/kicad-diff.py +++ b/kicad-diff.py @@ -29,7 +29,7 @@ __copyright__ = 'Copyright 2020-2022, INTI/'+__author__ __credits__ = ['Salvador E. Tropea', 'Jesse Vincent'] __license__ = 'GPL 2.0' -__version__ = '2.0.0' +__version__ = '2.1.0' __email__ = 'salvador@inti.gob.ar' __status__ = 'beta' @@ -42,12 +42,11 @@ from pcbnew import LoadBoard, PLOT_CONTROLLER, FromMM, PLOT_FORMAT_PDF, Edge_Cuts, GetBuildVersion import re from shutil import rmtree, which -from subprocess import call +from subprocess import call, PIPE, run, STDOUT from sys import exit -from tempfile import mkdtemp +from tempfile import mkdtemp, NamedTemporaryFile import time -MAX_LAYERS = 50 # Exit error codes OLD_INVALID = 1 NEW_INVALID = 2 @@ -57,8 +56,11 @@ FAILED_TO_DIFF = 6 FAILED_TO_JOIN = 7 WRONG_EXCLUDE = 8 +WRONG_ARGUMENT = 9 +DIFF_TOO_BIG = 10 kicad_version_major = kicad_version_minor = kicad_version_patch = 0 is_pcb = True +use_poppler = True def GenPCBImages(file, file_hash, hash_dir, file_no_ext): @@ -134,26 +136,75 @@ def GenImages(file, file_hash): GenSCHImage(file, file_hash, hash_dir, file_no_ext) +def cmd_pdf2miff(name, res, dest='miff:-'): + if use_poppler: + return 'cat {} | pdftoppm -r {} -gray - | convert - {}'.format(name, res, dest) + return ('convert -density {} {} -background white -alpha remove -alpha off ' + '-threshold 50% -colorspace Gray -resample {} -depth 8 {}'.format(res*2, name, res, dest)) + + +def create_diff_stereo(old_name, new_name, diff_name, font_size, layer, resolution, name_layer): + text = ' -font helvetica -pointsize '+font_size+' -draw "text 10,'+font_size+' \''+name_layer+'\'" ' + conv_old = cmd_pdf2miff(old_name, resolution) + conv_new = cmd_pdf2miff(new_name, resolution) + command = ['bash', '-c', '('+conv_old+' ; '+conv_new+') | ' + + r'convert - \( -clone 0-1 -compose darken -composite \) '+text+' -channel RGB -combine '+diff_name] + logger.debug('Executing: '+str(command)) + run(command, check=True) + + +def create_diff_stat(old_name, new_name, diff_name, font_size, layer, resolution, name_layer): + with NamedTemporaryFile(suffix='.png') as old_f: + # Convert the old file + cmd = ['bash', '-c', cmd_pdf2miff(old_name, resolution, old_f.name)] + logger.debug('Executing: '+str(cmd)) + call(cmd) + with NamedTemporaryFile(suffix='.png') as new_f: + # Convert the new file + cmd = ['bash', '-c', cmd_pdf2miff(new_name, resolution, new_f.name)] + logger.debug('Executing: '+str(cmd)) + call(cmd) + # Compare both + cmd = ['compare', + # Tolerate 5 % error in color (configurable) + '-fuzz', str(args.fuzz)+'%', + # Count how many pixels differ + '-metric', 'AE', + new_f.name, + old_f.name, + '-colorspace', 'RGB', + diff_name] + logger.debug('Executing: '+str(cmd)) + res = run(cmd, stdout=PIPE, stderr=STDOUT) + errors = int(res.stdout.decode()) + logger.debug('AE for {}: {}'.format(layer, errors)) + if args.thresold and errors > args.thresold: + logger.error('Difference for `{}` is not acceptable ({} > {})'.format(name_layer, errors, args.thresold)) + exit(DIFF_TOO_BIG) + cmd = ['convert', diff_name, '-font', 'helvetica', '-pointsize', font_size, '-draw', + 'text 10,'+font_size+" '"+name_layer+"'", diff_name] + logger.debug('Executing: '+str(cmd)) + call(cmd) + + def DiffImages(old_file, old_file_hash, new_file, new_file_hash): old_hash_dir = cache_dir+sep+old_file_hash new_hash_dir = cache_dir+sep+new_file_hash files = ['convert'] # Compute the difference between images for each layer, store JPGs - res = '-r '+str(resolution) font_size = str(int(resolution/5)) for i in sorted(layer_names.keys()): layer = layer_names[i] + name_layer = layer if layer == 'Schematic' else 'Layer: '+layer layer_rep = layer.replace('.', '_') old_name = '%s%s%s.pdf' % (old_hash_dir, sep, layer_rep) new_name = '%s%s%s.pdf' % (new_hash_dir, sep, layer_rep) diff_name = '%s%s%s-%s.png' % (output_dir, sep, 'diff', layer_rep) logger.info('Creating diff for %s' % layer) - text = ' -font helvetica -pointsize '+font_size+' -draw "text 10,'+font_size+' \'Layer: '+layer+'\'" ' - command = ['bash', '-c', '(cat '+old_name+' | pdftoppm '+res+' -gray - | convert - miff:- ; ' + - 'cat '+new_name+' | pdftoppm '+res+' -gray - | convert - miff:-) | ' + - r'convert - \( -clone 0-1 -compose darken -composite \) '+text+' -channel RGB -combine '+diff_name] - logger.debug(command) - call(command) + if args.diff_mode == 'red_green': + create_diff_stereo(old_name, new_name, diff_name, font_size, layer, resolution, name_layer) + else: + create_diff_stat(old_name, new_name, diff_name, font_size, layer, resolution, name_layer) if not isfile(diff_name): logger.error('Failed to create diff %s' % diff_name) exit(FAILED_TO_DIFF) @@ -216,19 +267,34 @@ def load_layer_names(old_file): return layer_names +def thre_type(astr, min=0, max=1e6): + value = int(astr) + if min <= value <= max: + return value + else: + raise argparse.ArgumentTypeError('value not in range %s-%s'%(min, max)) + + if __name__ == '__main__': parser = argparse.ArgumentParser(description='KiCad diff') parser.add_argument('old_file', help='Original file (PCB/SCH)') parser.add_argument('new_file', help='New file (PCB/SCH)') parser.add_argument('--cache_dir', nargs=1, help='Directory to cache images') - parser.add_argument('--output_dir', nargs=1, help='Directory for the output files') - parser.add_argument('--resolution', nargs=1, help='Image resolution in DPIs [150]', default=['150']) - parser.add_argument('--old_file_hash', nargs=1, help='Use this hash for OLD_FILE') - parser.add_argument('--new_file_hash', nargs=1, help='Use this hash for NEW_FILE') + parser.add_argument('--diff_mode', help='How to compute the image difference [red_green]', + choices=['red_green', 'stats'], default='red_green') parser.add_argument('--exclude', nargs=1, help='Exclude layers in file (one layer per line)') - parser.add_argument('--verbose', '-v', action='count', default=0) + parser.add_argument('--force_gs', help='Use Ghostscript even when Poppler is available', action='store_true') + parser.add_argument('--fuzz', help='Color tollerance for diff stats mode [%(default)s]', type=int, choices=range(0,101), + default=5, metavar='[0-100]') + parser.add_argument('--new_file_hash', nargs=1, help='Use this hash for NEW_FILE') parser.add_argument('--no_reader', help='Don\'t open the PDF reader', action='store_false') + parser.add_argument('--old_file_hash', nargs=1, help='Use this hash for OLD_FILE') + parser.add_argument('--output_dir', nargs=1, help='Directory for the output files') + parser.add_argument('--resolution', help='Image resolution in DPIs [%(default)s]', type=int, default=150) + parser.add_argument('--thresold', help='Error thresold for diff stats mode, 0 is no error [%(default)s]', + type=thre_type, default=0, metavar='[0-1000000]') + parser.add_argument('--verbose', '-v', action='count', default=0) parser.add_argument('--version', action='version', version='%(prog)s '+__version__+' - ' + __copyright__+' - License: '+__license__) @@ -248,9 +314,12 @@ def load_layer_names(old_file): if which('convert') is None: logger.error('No convert command, install ImageMagick') exit(MISSING_TOOLS) + use_poppler = not args.force_gs if which('pdftoppm') is None: - logger.error('No pdftoppm command, install poppler-utils') - exit(MISSING_TOOLS) + if which('gs') is None: + logger.error('No pdftoppm or ghostscript command, install poppler-utils or ghostscript') + exit(MISSING_TOOLS) + use_poppler = False if which('xdg-open') is None: logger.warning('No xdg-open command, install xdg-utils. Disabling the PDF viewer.') args.no_reader = False @@ -306,8 +375,8 @@ def load_layer_names(old_file): logger.debug('Temporal output dir %s' % output_dir) atexit.register(CleanOutputDir) - resolution = int(args.resolution[0]) - if resolution < 30 and resolution > 400: + resolution = args.resolution + if resolution < 30 or resolution > 400: logger.warning('Resolution outside the recommended range [30,400]') layer_exclude = [] diff --git a/kicad-git-diff.py b/kicad-git-diff.py index 72ae5bc..f76aedb 100755 --- a/kicad-git-diff.py +++ b/kicad-git-diff.py @@ -15,7 +15,7 @@ __copyright__ = 'Copyright 2020, INTI' __credits__ = ['Salvador E. Tropea', 'Jesse Vincent'] __license__ = 'GPL 2.0' -__version__ = '2.0.0' +__version__ = '2.1.0' __email__ = 'salvador@inti.gob.ar' __status__ = 'beta' # PCB diff tool