From c10d643c1fd6ddfbd1eba60ee91fc4435462711b Mon Sep 17 00:00:00 2001 From: Werner Almesberger Date: Fri, 27 Aug 2010 03:54:26 -0300 Subject: [PATCH] Tools for making a browseable graphical revision history of schematics. - scripts/gitsch2ppm: extract schematics as PPM files from a specfific git revision - scripts/gitenealogy: show the commit history of a file, tracking renames - scripts/ppmdiff/Makefile, scripts/ppmdiff/ppmdiff.c: compare two PPM files and highlight differences - scripts/schhist2web: generate a browseable graphical revision history of schematics --- scripts/gitenealogy | 29 ++++ scripts/gitsch2ppm | 108 ++++++++++++ scripts/ppmdiff/Makefile | 3 + scripts/ppmdiff/ppmdiff.c | 356 ++++++++++++++++++++++++++++++++++++++ scripts/schhist2web | 164 ++++++++++++++++++ 5 files changed, 660 insertions(+) create mode 100755 scripts/gitenealogy create mode 100755 scripts/gitsch2ppm create mode 100644 scripts/ppmdiff/Makefile create mode 100644 scripts/ppmdiff/ppmdiff.c create mode 100755 scripts/schhist2web diff --git a/scripts/gitenealogy b/scripts/gitenealogy new file mode 100755 index 0000000..aa49bb7 --- /dev/null +++ b/scripts/gitenealogy @@ -0,0 +1,29 @@ +#!/bin/sh +# +# gitenealogy - Trace the ancestry of a file in git across renames +# +# Written 2010 by Werner Almesberger +# Copyright 2010 Werner Almesberger +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# + + +usage() +{ + echo "usage: $0 path" 2>&1 + exit 1 +} + + +[ -z "$1" -o ! -z "$2" ] && usage +[ ! -f "$1" ] && usage + +git log --follow --name-status "$1" | + awk ' +/^commit /{ if (c) print c, n; c = $2 } +{ if (NF) n = $(NF) } +END { if (c) print c, n; }' diff --git a/scripts/gitsch2ppm b/scripts/gitsch2ppm new file mode 100755 index 0000000..cbbca87 --- /dev/null +++ b/scripts/gitsch2ppm @@ -0,0 +1,108 @@ +#!/bin/sh +# +# gitsch2ppm - Generate PPM files for KiCad schematics in git +# +# Written 2010 by Werner Almesberger +# Copyright 2010 Werner Almesberger +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# + + +RES=1280x850 +LINEWIDTH=120 + + +ps2ppm() +{ + X=`echo $RES | sed 's/x.*//'` + Y=`echo $RES | sed 's/.*x//'` + IRES=${Y}x$X + res=`expr 72 \* $X / 800` + + ( cat <`dirname "$1"`/`basename "$1" .ps`.ppm +} + + +usage() +{ + cat <&2 +usage: $0 [options] top-dir top-schem [commit] outdir + + -r XxY image resolution (default: $RES) + -w points Postscript line width (default: $LINEWIDTH) +EOF + exit 1 +} + + +while true; do + case "$1" in + -r) [ -z "$2" ] && usage + RES="$2" + shift 2 + break;; + -w) [ -z "$2" ] && usage + LINEWIDTH="$2" + shift 2 + break;; + -*) + usage;; + *) + break;; + esac +done + +[ ! -z "$3" -a -z "$5" ] || usage +dir="$1" +schem="$2" +sdir=`dirname "$schem"` +if [ -z "$4" ]; then + commit=HEAD + outdir="$3" +else + commit="$3" + outdir="$4" +fi + +[ "$dir" != "${dir#/}" ] || dir=`pwd`/$dir + +[ "$commit" != HEAD -o -f "$dir/$schem" ] || usage +[ -d "$dir/.git" ] || usage + +tmp="$dir/../_schdiff_a" +sch="$tmp/$sdir" + +rm -rf "$tmp" + +git clone -s -n "$dir/.git" "$tmp" || exit +( cd "$tmp" && git checkout -q "$commit"; ) || exit + +if [ ! -f "$tmp/$schem" ]; then + echo "$schem not found (checked out into $tmp)" 1>&2 + exit 1 +fi + +( cd "$sch" && rm -f *.ps *.ppm && eeschema --plot "$tmp/$schem"; ) || exit + +for n in "$sch"/*.ps; do + ps2ppm "$n" +done + +rm -rf "$outdir" +mkdir -p "$outdir" + +mv "$sch"/*.ppm "$outdir" + +rm -rf "$tmp" diff --git a/scripts/ppmdiff/Makefile b/scripts/ppmdiff/Makefile new file mode 100644 index 0000000..451c513 --- /dev/null +++ b/scripts/ppmdiff/Makefile @@ -0,0 +1,3 @@ +CFLAGS=-Wall -g + +ppmdiff: diff --git a/scripts/ppmdiff/ppmdiff.c b/scripts/ppmdiff/ppmdiff.c new file mode 100644 index 0000000..0025836 --- /dev/null +++ b/scripts/ppmdiff/ppmdiff.c @@ -0,0 +1,356 @@ +/* + * ppmdiff.c - Mark differences in two PPM files + * + * Written 2010 by Werner Almesberger + * Copyright 2010 Werner Almesberger + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + + +#include +#include +#include +#include +#include + + +static uint8_t a_only[3] = { 255, 0, 0 }; +static uint8_t b_only[3] = { 0, 255, 0 }; +static uint8_t both[3] = { 220, 220, 220 }; +static uint8_t frame[3] = { 0, 0, 255 }; +static uint8_t frame_fill[3] = { 255, 255, 200 }; +static int frame_dist = 40; +static int frame_width = 2; + + +static uint8_t *load_ppm(const char *name, int *x, int *y) +{ + FILE *file; + char line[100]; + int this_x, this_y, depth; + int n; + uint8_t *img; + + file = fopen(name, "r"); + if (!file) { + perror(name); + exit(1); + } + if (!fgets(line, sizeof(line), file)) { + fprintf(stderr, "can't read file type\n"); + exit(1); + } + if (strcmp(line, "P6\n")) { + fprintf(stderr, "file type must be P6, not %s", line); + exit(1); + } + if (!fgets(line, sizeof(line), file)) { + fprintf(stderr, "can't read resolution\n"); + exit(1); + } + if (sscanf(line, "%d %d", &this_x, &this_y) != 2) { + fprintf(stderr, "can't parse resolution: %s", line); + exit(1); + } + if (*x || *y) { + if (*x != this_x || *y != this_y) { + fprintf(stderr, + "resolution changed from %dx%d to %dx%d\n", + *x, *y, this_x, this_y); + exit(1); + } + } else { + *x = this_x; + *y = this_y; + } + if (!fgets(line, sizeof(line), file)) { + fprintf(stderr, "can't read depth\n"); + exit(1); + } + if (sscanf(line, "%d", &depth) != 1) { + fprintf(stderr, "can't parse depth: %s", line); + exit(1); + } + if (depth != 255) { + fprintf(stderr, "depth must be 255, not %d\n", depth); + exit(1); + } + n = *x**y*3; + img = malloc(n); + if (!img) { + perror("malloc"); + exit(1); + } + if (fread(img, 1, n, file) != n) { + fprintf(stderr, "can't read %d bytes\n", n); + exit(1); + } + fclose(file); + return img; +} + + +static struct area { + int x0, y0, x1, y1; + struct area *next; +} *areas = NULL; + + +static void add_area(struct area **root, int x0, int y0, int x1, int y1) +{ + while (*root) { + struct area *area = *root; + + if (area->x0 > x1 || area->y0 > y1 || + area->x1 < x0 || area->y1 < y0) { + root = &(*root)->next; + continue; + } + x0 = x0 < area->x0 ? x0 : area->x0; + y0 = y0 < area->y0 ? y0 : area->y0; + x1 = x1 > area->x1 ? x1 : area->x1; + y1 = y1 > area->y1 ? y1 : area->y1; + *root = area->next; + free(area); + add_area(&areas, x0, y0, x1, y1); + return; + } + *root = malloc(sizeof(**root)); + if (!*root) { + perror("malloc"); + exit(1); + } + (*root)->x0 = x0; + (*root)->y0 = y0; + (*root)->x1 = x1; + (*root)->y1 = y1; + (*root)->next = NULL; +} + + +static void change(int x, int y) +{ + add_area(&areas, + x-frame_dist, y-frame_dist, x+frame_dist, y+frame_dist); +} + + +static void set_pixel(uint8_t *p, const uint8_t *color, const uint8_t *value) +{ + double f; + int i; + + f = (255-(value[0] | value[1] | value[2]))/255.0; + for (i = 0; i != 3; i++) + p[i] = 255-(255-color[i])*f; +} + + +static uint8_t *diff(const uint8_t *a, const uint8_t *b, int xres, int yres) +{ + uint8_t *res, *p; + int x, y; + int has_a, has_b; + + res = p = malloc(xres*yres*3); + if (!res) { + perror("malloc"); + exit(1); + } + for (y = 0; y != yres; y++) + for (x = 0; x != xres; x++) { + has_a = (a[0] & a[1] & a[2]) != 255; + has_b = (b[0] & b[1] & b[2]) != 255; + if (has_a && has_b) { + set_pixel(p, both, b); + } else if (has_a) { + set_pixel(p, a_only, a); + change(x, y); + } else if (has_b) { + set_pixel(p, b_only, b); + change(x, y); + } else { + memset(p, 255, 3); +// memcpy(p, "\0\0\xff", 3); + } + a += 3; + b += 3; + p += 3; + } + return res; +} + + +static void point(uint8_t *img, int x, int y, int xres, int yres) +{ + uint8_t *p; + + if (x < 0 || y < 0 || x >= xres || y >= yres) + return; + p = img+(y*xres+x)*3; + if ((p[0] & p[1] & p[2]) != 255) + return; + memcpy(p, frame, 3); +} + + +static void hline(uint8_t *img, int x0, int x1, int y, int xres, int yres) +{ + int x; + + for (x = x0; x <= x1; x++) + point(img, x, y, xres, yres); +} + + +static void vline(uint8_t *img, int y0, int y1, int x, int xres, int yres) +{ + int y; + + for (y = y0; y <= y1; y++) + point(img, x, y, xres, yres); +} + + +static void fill(uint8_t *img, int x0, int y0, int x1, int y1, + int xres, int yres) +{ + int x, y; + uint8_t *p; + + for (y = y0; y <= y1; y++) { + if (y < 0 || y >= yres) + continue; + p = img+(xres*y+x0)*3; + for (x = x0; x <= x1; x++) { + if (x >= 0 && x < xres && (p[0] & p[1] & p[2]) == 255) + memcpy(p, frame_fill, 3); + p += 3; + } + } +} + + +static void mark_areas(uint8_t *img, int x, int y) +{ + const struct area *area; + int r1 = 0, r2 = 0, i; + + if (frame_width) { + r1 = (frame_width-1)/2; + r2 = (frame_width-1)-r1; + } + for (area = areas; area; area = area->next) { + if (frame_width) + for (i = -r1; i <= r2; i++) { + hline(img, area->x0-r1, area->x1+r2, area->y0+i, + x, y); + hline(img, area->x0-r1, area->x1+r2, area->y1+i, + x, y); + vline(img, area->y0+r1, area->y1-r2, area->x0+i, + x, y); + vline(img, area->y0+r1, area->y1-r2, area->x1+i, + x, y); + } + fill(img, + area->x0+r1, area->y0+r1, area->x1-r2, area->y1-r2, x, y); + } +} + + +static void usage(const char *name) +{ + fprintf(stderr, +"usage: %s [-f] [-a color] [-b color] [-c color] [-d pixels]\n" +"%6s %*s [-m color] [-n color] [-w pixels] file_a.ppm file_b.ppm\n\n" +" -f generate output (and return success) even if there is no change\n" +" -a color color of items only in image A\n" +" -b color color of items only in image B\n" +" -c color color of items in both images\n" +" -d pixels distance between change and marker box. 0 disables markers.\n" +" -m color color of the frame of the marker box.\n" +" -n color color of the background of the marker box\n" +" -w pixels width of the frame of the marker box. 0 disables frames.\n\n" +" color is specified as R,B,G with each component as a floating-point\n" +" value from 0 to 1. E.g., 1,1,1 is white.\n\n" +" The images are expected to have dark colors on a perfectly white\n" +" background.\n" + , name, "", (int) strlen(name), ""); + exit(1); +} + + +static void parse_color(uint8_t *c, const char *s, const char *name) +{ + float r, g, b; + + if (sscanf(s, "%f,%f,%f", &r, &g, &b) != 3) + usage(name); + c[0] = 255*r; + c[1] = 255*g; + c[2] = 255*b; +} + + +int main(int argc, char *const *argv) +{ + int force = 0; + int x = 0, y = 0; + uint8_t *old, *new, *d; + char *end; + int c; + + while ((c = getopt(argc, argv, "a:b:c:d:fm:n:w:")) != EOF) + switch (c) { + case 'a': + parse_color(a_only, optarg, *argv); + break; + case 'b': + parse_color(b_only, optarg, *argv); + break; + case 'c': + parse_color(both, optarg, *argv); + break; + case 'd': + frame_dist = strtoul(optarg, &end, 0); + if (*end) + usage(*argv); + break; + case 'f': + force = 1; + break; + case 'm': + parse_color(frame, optarg, *argv); + break; + case 'n': + parse_color(frame_fill, optarg, *argv); + break; + case 'w': + frame_width = strtoul(optarg, &end, 0); + if (*end) + usage(*argv); + break; + default: + usage(*argv); + } + if (argc-optind != 2) + usage(*argv); + old = load_ppm(argv[optind], &x, &y); + new = load_ppm(argv[optind+1], &x, &y); + d = diff(old, new, x, y); + if (frame_dist) + mark_areas(d, x, y); + if (!areas && !force) + return 1; + printf("P6\n%d %d\n255\n", x, y); + fwrite(d, 1, x*y*3, stdout); + if (fclose(stdout) == EOF) { + perror("fclose"); + exit(1); + } + return 0; +} diff --git a/scripts/schhist2web b/scripts/schhist2web new file mode 100755 index 0000000..560f832 --- /dev/null +++ b/scripts/schhist2web @@ -0,0 +1,164 @@ +#!/bin/sh + +THUMB_OPTS="-w 3 -d 60 -n 1,1,0" + + +shrink() +{ + pnmscale -width 120 "$@" || exit +} + + +pngdiff() +{ + # pngdiff preproc outfile arg ... + pp="$1" + of="$2" + shift 2 + if ! PATH=$PATH:`dirname $0`/ppmdiff ppmdiff "$@" >"$out/_tmp"; then + rm "$out/_tmp" + return 1 + fi + $pp "$out/_tmp" | pnmtopng >"$of" + rm -f "$out/_tmp" +} + + +usage() +{ + echo "usage: $0 [top-dir] [top-schem] [outdir]" 2>&1 + exit 1 +} + + +if [ ! -z "$1" -a -d "$1/.git" ]; then + dir="$1" + shift +else + dir=. + while [ ! -d $dir/.git ]; do + if [ $dir -ef $dir/.. ]; then + echo "no .git/ directory found in hierarchy" 1>&2 + exit 1 + fi + dir=$dir/.. + done +fi + +if [ ! -z "$1" -a -f "$dir/$1" -a \ + -f "$dir"/`dirname "$1"`/`basename "$1" .sch`.pro ]; then + sch="$1" + shift +else + for n in "$dir"/*.sch; do + [ -f `dirname "$n"`/`basename "$n" .sch`.pro ] || continue + if [ ! -z "$sch" ]; then + echo "multiple choices for top-level .sch file" 1>&2 + exit 1 + fi + sch="$n" + done + if [ -z "$sch" -o "$sch" = "$dir/*.sch" ]; then + echo "no candidate for top-level .sch file found" 1>&2 + exit 1 + fi +fi + +if [ ! -z "$1" ] && [ ! -e "$1" ] || [ -d "$1" -a ! -d "$1"/.git ]; then + out="$1" + shift +else + out=_out +fi + +[ -z "$1" ] || usage + +PATH=`dirname "$0"`:"$PATH" +first=`gitenealogy "$dir/$sch" | sed '$s/ .*//p;d'` +schname=`gitenealogy "$dir/$sch" | sed '$s/^.* //p;d'` + +# @@@ POOR MAN'S CACHE +if true; then + +rm -rf "$out" +mkdir -p "$out/names" + +for n in $first `git rev-list --reverse $first..HEAD`; do +echo Processing $n + new=`gitenealogy "$dir/$sch" | sed "/^$n /s///p;d"` + if [ ! -z "$new" ]; then +echo Name change $schname to $new + schname="$new" + fi + mkdir "$out/ppm_$n" + gitsch2ppm "$dir" "$schname" $n "$out/ppm_$n" || exit + gitsch2ppm -w 500 "$dir" "$schname" $n "$out/fat_$n" || exit + for m in "$out/ppm_$n/"*; do + [ "$m" = "$out/ppm_$n/*" ] && break + touch "$out/names/"`basename "$m" .ppm` + done +done + +fi + +cat <"$out/index.html" + + + + +EOF +for m in `ls -1 "$out/names"`; do + echo "" + mkdir -p "$out/diff_$next" "$out/thumb_$next" + for m in `ls -1 "$out/names"`; do + a="$out/ppm_$n/$m.ppm" + fat_a="$out/fat_$n/$m.ppm" + b="$out/ppm_$next/$m.ppm" + fat_b="$out/fat_$next/$m.ppm" + diff="$out/diff_$next/$m.png" + thumb="$out/thumb_$next/$m.png" + + if [ -f "$a" -a -f "$b" ]; then + s="$s
$m" >>"$out/index.html" +done + +head=`git rev-list HEAD~1..HEAD` +next="$head" +for n in `git rev-list $first..HEAD~1` $first; do + empty=true + s="
" + pngdiff cat "$diff" "$a" "$b" || continue + pngdiff shrink "$thumb" -f $THUMB_OPTS "$fat_a" "$fat_b" || exit + elif [ -f "$a" ]; then + s="$s" + pngdiff cat "$diff" -f -c 1,0,0 "$a" || exit + pngdiff shrink "$thumb" -f -c 1,0,0 $THUMB_OPTS "$fat_a" || exit + elif [ -f "$out/$next/$m.ppm" ]; then + s="$s" + pngdiff cat "$diff" -f -c 0,1,0 "$b" || exit + pngdiff shrink "$thumb" -f -c 0,1,0 $THUMB_OPTS "$fat_b" || exit + else + continue + fi + echo "$s" >>"$out/index.html" + s= + empty=false + echo "" >>"$out/index.html" + done + if ! $empty; then + ( + cat < +
+EOF + mkdir -p "$out/diff_$next" "$out/thumb_$next" + echo "
"
+	    git log --pretty=short $n..$next
+	    echo "
" + ) >>"$out/index.html" + fi + next=$n +done + +echo "
" >>"$out/index.html" +exit 1