-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathrender-graph
More file actions
executable file
·583 lines (517 loc) · 16.1 KB
/
render-graph
File metadata and controls
executable file
·583 lines (517 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
#!/bin/bash
########################
DEBUG="false"
DELETE_TMP_FILES="false"
########################
#####################################################################
SIZEY="40" # How tall is one cell.
SIZEY_MIN="2"
SIZEY_MAX="62" # If larger, ImageMagick's bug appears. (v6.7.7-10)
TAB_WIDTH="4" # Need to be set correctly if input file contains tabs.
COLOR="true" # Colorize the labels using standard git colors.
PRINT_HELP="false"
PRINT_VERSION="false"
DRAW_EXAMPLE="false"
DONT_ANTIALIAS_HOR_AND_VER_LINES="true"
#####################################################################
EXAMPLE=' origin/head origin/master
\ /
HEAD-(D)-master
/ | \
(B)(C) v1.0
| /
(A)
|
---'
APP_NAME=`basename "$0"`
USAGE="$APP_NAME [OPTION]... [FILE]"
DESCRIPTION="Reads a graph from ascii drawing and creates PNG file using ImageMagick's
convert command. Intended for drawings of git history graphs.
-s, --size=SIZE
set height of one cell in pixels, default value is $SIZEY.
Will accept values between $SIZEY_MIN and $SIZEY_MAX.
-o, --output=FILENAME
choose the filename for the output file. The .png suffix will
be appended at the end of the name. Default output filename
is the input filename with the .png suffix.
-t, --tab-width=SIZE
set number of spaces tabs in input file are gonna be
converted to, default value is $TAB_WIDTH
-c, --no-color
do not colorize the labels of the graph
-h, --help
display this help and exit
-e, --example
create picture from the example graph below. If output filename
is not specified, it gets saved as example.png
$EXAMPLE
-v, --version output version information and exit\n"
HOMEPAGE_BUGS="https://github.com/gto76/ascii-git-graph-to-png/issues"
AUTHOR="Jure Sorn"
COPYRIGHT="Copyright (c) 2014 Jure Sorn. The MIT License (MIT)."
VERSION="v0.9.0"
#########################
#### PARSE ARGUMENTS ####
#########################
parseArguments() {
# read the options
TEMP=`getopt -o hvces:o:t: --long help,version,no-color,example,size:,output:,tab-width: -n 'test.sh' -- "$@"`
eval set -- "$TEMP"
# extract options and their arguments into variables.
while true ; do
case "$1" in
-h|--help) PRINT_HELP="true" ; shift ;;
-v|--version) PRINT_VERSION="true" ; shift ;;
-c|--no-color) COLOR="false" ; shift ;;
-e|--example) DRAW_EXAMPLE="true" ; shift ;;
-s|--size)
case "$2" in
"") shift 2 ;;
*) SIZEY=$2 ; shift 2 ;;
esac ;;
-o|--output)
case "$2" in
"") shift 2 ;;
*) OUTPUT_FILE_NAME=$2.png ; shift 2 ;;
esac ;;
-t|--tab-width)
case "$2" in
"") shift 2 ;;
*) TAB_WIDTH=$2 ; shift 2 ;;
esac ;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
shift $(expr $OPTIND - 1 )
FILE_NAME="$1"
}
##########################
#### VERIFY ARGUMENTS ####
##########################
verifyArguments() {
if [[ "$PRINT_HELP" == "true" ]]; then
echo -e "Usage: $USAGE"; echo -e "$DESCRIPTION"
echo -e "Report bugs to $HOMEPAGE_BUGS"
echo -e "$COPYRIGHT"
exit 0
elif [[ "$PRINT_VERSION" == "true" ]]; then
echo "$APP_NAME $VERSION"
exit 0
elif [[ "$DRAW_EXAMPLE" == "false" && -z "$FILE_NAME" ]]; then
echo "$APP_NAME: Filename was not specified" >&2
echo "Try '$APP_NAME --help' for more information." >&2
exit 1
elif [[ "$DRAW_EXAMPLE" == "false" && ! -e "$FILE_NAME" ]]; then
echo "$APP_NAME: failed to access ‘$FILE_NAME’: No such file or directory" >&2
exit 1
elif [[ "$DRAW_EXAMPLE" == "false" && ! -r "$FILE_NAME" ]]; then
echo "$APP_NAME: $FILE_NAME: Permission denied" >&2
exit 1
elif [[ -z `echo "$SIZEY" | grep -P '^[0-9]+$'` ]]; then
echo "$APP_NAME: Invalid size argument" >&2
exit 1
elif [[ "$SIZEY" -lt "$SIZEY_MIN" || "$SIZEY" -gt "$SIZEY_MAX" ]]; then
echo "$APP_NAME: Size argument is out of range, use values between $SIZEY_MIN and $SIZEY_MAX" >&2
exit 1
elif [[ -z `echo "$TAB_WIDTH" | grep -P '^[0-9]+$'` ]]; then
echo "$APP_NAME: Invalid tab width" >&2
exit 1
elif [[ "$TAB_WIDTH" -lt 1 || "$TAB_WIDTH" -gt 20 ]]; then
echo "$APP_NAME: Tab width out of range, use values between 1 and 20" >&2
exit 1
fi
}
#######################
#### SET CONSTANTS ####
#######################
setConstants() {
FONT="DejaVu-Sans-Mono-Bold-Oblique"
NODE_FONT="Liberation-Mono-Bold"
# Width of one ascii cell in pixels.
SIZEX=`echo "scale=1;$SIZEY*2/3" | bc | sed 's/^\./0\./' | sed 's/\..*//'`
# Height of a font.
let FONT_SIZE=("$SIZEY"*3)/4
# Distance from rectangles edge to the edge of the cell in the pixels.
let RECTANGLE_OFFSET="$SIZEX"/8
# An estimate of font-width/font-height.
FONT_WIDTH_FACTOR="0.6107"
# Additional heightening of the text expressed in height of a text
VERTICAL_LIFT_FACTOR="0.13"
VERTICAL_LIFT_EXPRESSION="scale=1;$FONT_SIZE*$VERTICAL_LIFT_FACTOR"
VERTICAL_LIFT=`echo "$VERTICAL_LIFT_EXPRESSION" \
| bc \
| sed 's/^\./0\./' \
| sed 's/\..*//'`
let RECTANGLE_HEIGHT="$SIZEY"-2*"$RECTANGLE_OFFSET"
# How far from the upper cells will be texts bottom.
TEXT_OFFSET_Y_EXPRESSION="scale=1;($RECTANGLE_HEIGHT-$FONT_SIZE)/2+$RECTANGLE_OFFSET+$FONT_SIZE-$VERTICAL_LIFT"
TEXT_OFFSET_Y=`echo "$TEXT_OFFSET_Y_EXPRESSION" \
| bc \
| sed 's/^\./0\./' \
| sed 's/\..*//'`
# Width of the lines in pixels.
#STROKE_WIDTH=`echo "scale=1;$SIZEY/30" | bc`
STROKE_WIDTH=`echo "scale=1;($SIZEY+15)/30" \
| bc \
| sed 's/^\./0\./' \
| sed 's/\..*//'`
if [[ -z "$OUTPUT_FILE_NAME" && "$DRAW_EXAMPLE" == "true" ]]; then
OUTPUT_FILE_NAME=example.png
elif [[ -z "$OUTPUT_FILE_NAME" && "$DRAW_EXAMPLE" == "false" ]]; then
OUTPUT_FILE_NAME="$FILE_NAME".png
fi
}
###############################
#### COPY LINES INTO ARRAY ####
###############################
copyLinesIntoArray() {
if [[ "$DRAW_EXAMPLE" == "true" ]]; then
INPUT_TEXT=`echo "$EXAMPLE" | tr -d '\t'`
else
INPUT_TEXT=`expand -t "$TAB_WIDTH" "$1"`
fi
SAVE_IFS="$IFS"
IFS=$'\n'
i=0
for a in `echo -e "$INPUT_TEXT"`; do
lines["$i"]="$a"
let i="$i"+1
done
IFS="$SAVE_IFS"
}
###############################
#### CALCULATE CANVAS SIZE ####
###############################
calculateCanvasSize() {
let canvasSizeY=${#lines[@]}*"$SIZEY"
canvasSizeX=0
for (( y=0 ; y < ${#lines[@]} ; y++ )); do
if [[ ${#lines["$y"]} -gt "canvasSizeX" ]]; then
canvasSizeX=${#lines["$y"]}
fi
done
let canvasSizeX="$canvasSizeX"*"$SIZEX"
}
#######################
#### PROCESS LABEL ####
#######################
setColor() {
color="rgb(138, 226, 52)" #branch -> lime
if [[ -n `echo "$1" | grep -i '^origin'` ]]; then
color="rgb(239, 41, 41)" #remote -> red
elif [[ -n `echo "$1" | grep -i '^head'` ]]; then
color="rgb(52, 226, 226)" #HEAD -> aqua
elif [[ -n `echo "$1" | grep -i '^v[0-9]\|^[0-9]'` ]]; then
color="rgb(252, 233, 77)" #tag -> yellow
fi
}
# Reads label until it reaches connector or space
# Draws rectangle and label on it.
processLabel() {
restOfLine=${lines[$y]:$x}
label=`echo "$restOfLine" | grep -Po '^[^ +()|\n\-\\\]*'`
length=${#label}
let newX="$x"+"$length"
let yPlus1="$y"+1
# RECTANGLE:
let rectangleX="$x"*"$SIZEX"+"$RECTANGLE_OFFSET"
let rectangleY="$y"*"$SIZEY"+"$RECTANGLE_OFFSET"
let rectangleX2="$newX"*"$SIZEX"-"$RECTANGLE_OFFSET"
let rectangleY2="$yPlus1"*"$SIZEY"-"$RECTANGLE_OFFSET"
if [[ "$COLOR" == "true" ]]; then
setColor "$label"
instructionRectangle="rectangle#fill '$color' rectangle $rectangleX,$rectangleY $rectangleX2,$rectangleY2 fill 'white'\n"
else
instructionRectangle="rectangle $rectangleX,$rectangleY $rectangleX2,$rectangleY2\n"
fi
# TEXT:
# position x:
textWidthExpression="scale=1;$FONT_SIZE*$length*$FONT_WIDTH_FACTOR"
textWidth=$(echo "$textWidthExpression" \
| bc \
| sed 's/^\./0\./' \
| sed 's/\..*//')
let rectangleWidth="$length"*"$SIZEX"-2*"$RECTANGLE_OFFSET"
let textOffsetX=("$rectangleWidth"-"$textWidth")/2+"$RECTANGLE_OFFSET"
let textX="$x"*"$SIZEX"+"$textOffsetX"
# position y;
let textY="$y"*"$SIZEY"+"$TEXT_OFFSET_Y"
instructionText="text $textX,$textY '$label'\n"
instructions="$instructions""$instructionRectangle""$instructionText"
let x="$newX"-1 # Because x will get incremented at the start of next loop
}
######################
#### PROCESS NODE ####
######################
# Draws a circle and nodes label on it.
processNode() {
let xPlus1="$x"+1
let yPlus1="$y"+1
label=${lines[$y]:$xPlus1:1}
# Circle:
let circleX="$xPlus1"*"$SIZEX"+"$SIZEX"/2
let circleY="$y"*"$SIZEY"+"$SIZEY"/2
let circleY2="$y"*"$SIZEY"
instructionCircle="m#circle $circleX,$circleY $circleX,$circleY2\n"
# Text:
#let labelX="$xPlus1"*"$SIZEX"
length=1
textWidthExpression="scale=1;$FONT_SIZE*$length*$FONT_WIDTH_FACTOR"
textWidth=$(echo "$textWidthExpression" \
| bc \
| sed 's/^\./0\./' \
| sed 's/\..*//')
let rectangleWidth="$length"*"$SIZEX"-2*"$RECTANGLE_OFFSET"
let textOffsetX=("$rectangleWidth"-"$textWidth")/2+"$RECTANGLE_OFFSET"
let textX="$xPlus1"*"$SIZEX"+"$textOffsetX"
#let labelY="$yPlus1"*"$SIZEY"-"$SIZEY"/4
let textY="$y"*"$SIZEY"+"$TEXT_OFFSET_Y"
instructionText="text#font '$NODE_FONT' text $textX,$textY '$label' font '$FONT'\n"
instructions="$instructions""$instructionCircle""$instructionText"
let x="$x"+2
}
#######################
#### PROCESS ARROW ####
#######################
drawLine() {
if [[ "$antialias" == "false" ]]; then
instruction="line#stroke-antialias 0 line $endpoint1 $endpoint2 stroke-antialias 1"
else
instruction="line $endpoint1 $endpoint2"
fi
instructions="$instructions""$instruction""\n"
}
# Reads line until the end of an arrow. Draws an arrow.
processArrow() {
restOfLine=${lines[$y]:$x}
arrow=`echo "$restOfLine" | grep -oP '^_*\\\\'`
length=${#arrow}
let newX="$x"+"$length"
# Draw the horizontal line.
let scaledX="$x"*"$SIZEX"
let yPlus1="$y"+1
let scaledYPlus1="$yPlus1"*"$SIZEY"
endpoint1="$scaledX,$scaledYPlus1"
let scaledNewX="$newX"*"$SIZEX"
endpoint2="$scaledNewX,$scaledYPlus1"
drawLine
# Draw upper angeled line.
let newXMinus1="$newX"-1
let scaledNewXMinus1="$newXMinus1"*"$SIZEX"
let scaledY="$y"*"$SIZEY"
endpoint1="$scaledNewXMinus1,$scaledY"
drawLine
# Draw lower angled line.
let yPlus2="$y"+2
let scaledYPlus2="$yPlus2"*"$SIZEY"
endpoint1="$scaledNewXMinus1,$scaledYPlus2"
drawLine
let x="$newX"-1 # Because x will get incremented at the start of next loop
}
######################
#### PROCESS EDGE ####
######################
# Investigates where does edge lead and draws a line between the ends.
processEdge() {
local char=${lines[$y]:$x:1}
if [[ "$char" == '|' ]]; then
findEdgesEndpoints 0 -1 0 1 "$char"
elif [[ "$char" == '-' ]]; then
findEdgesEndpoints -1 0 1 0 "$char"
elif [[ "$char" == '/' ]]; then
findEdgesEndpoints 1 -1 -1 1 "$char"
elif [[ "$char" == '\' ]]; then
findEdgesEndpoints -1 -1 1 1 "$char"
fi
if [[ "$DONT_ANTIALIAS_HOR_AND_VER_LINES" == true ]]; then
# If line is vertical or horizontal then turn antialiasing off
setAntialiasIfLineHorizontalOrVertical "$endpoint1" "$endpoint2"
fi
if [[ "$antialias" == "false" ]]; then
instruction="line#stroke-antialias 0 line $endpoint1 $endpoint2 stroke-antialias 1"
else
instruction="line $endpoint1 $endpoint2"
fi
instructions="$instructions""$instruction""\n"
}
# Finds the two endpoints of the edge
# Arguments:
# $1 - delta x of first direction, $2 - delta y of first direction,
# $3 - delta x of opposite direction, $4 - delta y of opposite direction
# $5 - the character representing the edge
findEdgesEndpoints() {
connectToX="$x"; connectToY="$y"
findEdgesEndpoint "$1" "$2" "$5"
let connectToX="$connectToX"*"$SIZEX"+"$SIZEX"/2
let connectToY="$connectToY"*"$SIZEY"+"$SIZEY"/2
endpoint1="$connectToX,$connectToY"
connectToX="$x"; connectToY="$y"
findEdgesEndpoint "$3" "$4" "$5"
let connectToX="$connectToX"*"$SIZEX"+"$SIZEX"/2
let connectToY="$connectToY"*"$SIZEY"+"$SIZEY"/2
endpoint2="$connectToX,$connectToY"
}
# Finds the endpoint in the specified direction
# $1 - delta x of direction, $2 - delta y of direction,
# $3 - the character representing the edge
findEdgesEndpoint() {
let newX="$connectToX"+"$1"
let newY="$connectToY"+"$2"
local char=${lines[$newY]:$newX:1}
# If char is empty or whitespace then return
if [[ -z "$char" ]]; then
return
fi
if [[ "$char" == ' ' ]]; then
# Check if char is not whitespace between two parenthesis (empty node)
let newXMinus1="$newX"-1
leftChar=${lines[$newY]:$newXMinus1:1}
if [[ "$leftChar" == '(' ]]; then
connectToX="$newX"
connectToY="$newY"
fi
return
fi
# If this field has the same edge character, continue to next one
if [[ "$char" == "$3" ]]; then
connectToX="$newX"
connectToY="$newY"
findEdgesEndpoint "$@"
elif [[ "$char" == '(' ]]; then
let connectToX="newX"+1
let connectToY="newY"
return
elif [[ "$char" == ')' ]]; then
let connectToX="newX"-1
let connectToY="newY"
return
else
connectToX="$newX"
connectToY="$newY"
return
fi
}
setAntialiasIfLineHorizontalOrVertical() {
local x1=`echo "$1" | sed 's/,.*$//'`
local y1=`echo "$1" | sed 's/^[0-9]*,//'`
local x2=`echo "$2" | sed 's/,.*$//'`
local y2=`echo "$2" | sed 's/^[0-9]*,//'`
if [[ "$x1" -eq "$x2" || "$y1" -eq "$y2" ]]; then
antialias="false"
else
antialias="true"
fi
}
###########################
#### SORT INSTRUCTIONS ####
###########################
# Sort instructions so that edges get drawn first, then circles
# and rectangles and lastly the labels.
sortInstructions() {
drawingInstructions=`echo -e "$instructions" \
| grep -v '^text' \
| sort -u \
| sed 's/^.*#//'`
textInstructions=`echo -e "$instructions" \
| grep '^text' \
| sed 's/^.*#//' \
| sort -u`
if [[ "$DEBUG" == "true" ]]; then
echo -e "\nINSTRUCTIONS:\n$drawingInstructions\n$textInstructions"
fi
}
####################
#### CREATE PNG ####
####################
createPng() {
echo -e "push graphic-context\n" > tmp.mvg
echo -e "viewbox 0 0 $canvasSizeX $canvasSizeY" >> tmp.mvg
echo -e "fill 'white'" >> tmp.mvg
echo -e "stroke 'black'" >> tmp.mvg
echo -e "stroke-antialias 1" >> tmp.mvg
echo -e "stroke-width $STROKE_WIDTH" >> tmp.mvg
echo -e "$drawingInstructions\n" >> tmp.mvg
echo -e "push graphic-context" >> tmp.mvg
echo -e "fill 'black'" >> tmp.mvg
echo -e "font-size $FONT_SIZE" >> tmp.mvg
echo -e "font '$FONT'" >> tmp.mvg
echo -e "text-antialias 1" >> tmp.mvg
echo -e "stroke-opacity 0" >> tmp.mvg
echo -e "$textInstructions\n" >> tmp.mvg
echo -e "pop graphic-context" >> tmp.mvg
echo -e "pop graphic-context" >> tmp.mvg
convert tmp.mvg "$OUTPUT_FILE_NAME"
if [[ "$DELETE_TMP_FILES" == "true" ]]; then
rm tmp.mvg
fi
}
##############
#### MAIN ####
##############
main() {
parseArguments "$@"
verifyArguments
setConstants
copyLinesIntoArray "$FILE_NAME"
calculateCanvasSize
# Drawing instructions for the ImageMagick
instructions=""
if [[ "$DEBUG" == "true" ]]; then
echo "STROKE_WIDTH = $STROKE_WIDTH\n"
echo "INPUT FILE:"
echo "$INPUT_TEXT"
echo -e "\nPARSING:"
fi
# Parse.
for (( y=0 ; y < ${#lines[@]} ; y++ )); do
if [[ "$DEBUG" == "true" ]]; then
echo -e "\nLine no $y: \"${lines[$y]}\""
fi
for (( x=0 ; x < ${#lines[y]} ; x++ )); do
# Get char at x,y, analyze it and act accordingly
char=${lines[$y]:$x:1}
if [[ "$DEBUG" == "true" ]]; then
echo -en "\n$x" "$y" \""$char"\"' '
fi
# Every symbol that is not space or reserved for
# node, arrow or edge is considered a beginning of
# an label
if [[ '-|/\\+()_ ' != *"$char"* ]]; then
if [[ "$DEBUG" == "true" ]]; then
echo -n label
fi
processLabel
fi
# Node looks like this: (A)
if [[ "$char" == '(' ]]; then
if [[ "$DEBUG" == "true" ]]; then
echo -n node
fi
processNode
fi
# Arrow looks like this: ___\
# /
if [[ "$char" == '_' ]]; then
if [[ "$DEBUG" == "true" ]]; then
echo -n arrow
fi
processArrow
fi
# Edges: | \ / ---
# | \ /
if [[ '-|/\' == *"$char"* ]]; then
if [[ "$DEBUG" == "true" ]]; then
echo -n edge
fi
processEdge
fi
done
if [[ "$DEBUG" == "true" ]]; then
echo
fi
done
sortInstructions
createPng
}
main "$@"