Skip to content

Commit

Permalink
Added "stats" mode
Browse files Browse the repository at this point in the history
- Compares using AE mode
- Also added support for GS as alternative to Poppler
- Bumped version to 2.1.0
  • Loading branch information
set-soft committed Aug 15, 2022
1 parent 47570c9 commit cdc7021
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 27 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
7 changes: 7 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -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 <[email protected]> Mon, 15 Aug 2022 18:27:50 -0300

kicad-pcb-diff (2.0.0-1) stable; urgency=medium

* Added SCH support
Expand Down
2 changes: 1 addition & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion kicad-diff-init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = '[email protected]'
__status__ = 'beta'

Expand Down
109 changes: 89 additions & 20 deletions kicad-diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = '[email protected]'
__status__ = 'beta'

Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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__)

Expand All @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down
2 changes: 1 addition & 1 deletion kicad-git-diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = '[email protected]'
__status__ = 'beta'
# PCB diff tool
Expand Down

0 comments on commit cdc7021

Please sign in to comment.