Last active
August 29, 2015 14:05
-
-
Save ajstarks/78f5d063571700da6d3a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// pbox -- porportional box | |
package main | |
import ( | |
"encoding/xml" | |
"fmt" | |
"github.com/ajstarks/svgo" | |
"io" | |
"os" | |
) | |
var canvas = svg.New(os.Stdout) | |
// <pbox width="1024" height="768" inset="1"> | |
// <section x="0" y="0" w="33.3" h="66.7" bgcolor="purple" tcolor="white"/> | |
// <section x="33.3" y="0" w="33.3" h="33.3" bgcolor="green" tcolor="white"/> | |
// <section x="33.3" y="33.3" w="33.3" h="33.3" bgcolor="orange" tcolor="white"/> | |
// <section x="33.3" y="66.7" w="33.3" h="33.3" bgcolor="green" tcolor="white"/> | |
// <section x="0" y="66.7" w="33.3" h="33.3" bgcolor="orange" tcolor="white"/> | |
// <section x="66.7" y="0" w="33.3" h="33.3" bgcolor="green" tcolor="white"/> | |
// <section x="66.7" y="33.3" w="33.3" h="66.7" bgcolor="red" tcolor="white"/> | |
// </pbox> | |
// data defintion | |
type Pbox struct { | |
Width int `xml:"width,attr"` | |
Height int `xml:"height,attr"` | |
Inset int `xml:"inset,attr"` | |
Tsize int `xml:"tsize,attr"` | |
Tunit string `xml:"tunit,attr"` | |
Tcolor string `xml:"tcolor,attr"` | |
Font string `xml:"font,attr"` | |
Bgcolor string `xml:"bgcolor,attr"` | |
Sections []slist `xml:"section"` | |
} | |
type section struct { | |
dx, dy, dw, dh int | |
X float64 `xml:"x,attr"` | |
Y float64 `xml:"y,attr"` | |
W float64 `xml:"w,attr"` | |
H float64 `xml:"h,attr"` | |
Tsize float64 `xml:"tsize,attr"` | |
Id string `xml:"id,attr"` | |
Label string `xml:"label,attr"` | |
Bgcolor string `xml:"bgcolor,attr"` | |
Tcolor string `xml:"tcolor,attr"` | |
Tstyle string `xml:"tstyle,attr"` | |
Align string `xml:"align,attr"` | |
Href string `xml:"href,attr"` | |
Content string `xml:",chardata"` | |
} | |
type slist struct { | |
section | |
Item []titem `xml:"item"` | |
} | |
type titem struct { | |
Start float64 `xml:"start,attr"` | |
End float64 `xml:"end,attr"` | |
Content string `xml:",chardata"` | |
} | |
// SVG document | |
type SVG struct { | |
Width int `xml:"width,attr"` | |
Height int `xml:"height,attr"` | |
Doc string `xml:",innerxml"` | |
} | |
const ( | |
defunit = "px" | |
globalstyle = "font-family:%s;font-size:%d%s;fill:%s" | |
stextstyle = "baseline-shift:-33%%;text-anchor:middle;font-size:%f%%;fill:%s" | |
) | |
// dopbox processes a pbox file | |
func dopbox(location string) { | |
var f *os.File | |
var err error | |
if len(location) > 0 { | |
f, err = os.Open(location) | |
} else { | |
f = os.Stdin | |
} | |
if err == nil { | |
readpbox(f) | |
f.Close() | |
} else { | |
fmt.Fprintf(os.Stderr, "%v\n", err) | |
} | |
} | |
// readpbox reads the pbox specification | |
func readpbox(r io.Reader) { | |
var p Pbox | |
if err := xml.NewDecoder(r).Decode(&p); err == nil { | |
drawpbox(p) | |
} else { | |
fmt.Fprintf(os.Stderr, "Unable to parse components (%v)\n", err) | |
} | |
} | |
// drawpbox draws the pbox, given the unmarshalled data | |
func drawpbox(p Pbox) { | |
if p.Bgcolor == "" { | |
p.Bgcolor = "black" | |
} | |
if p.Tcolor == "" { | |
p.Tcolor = "black" | |
} | |
if p.Tunit == "" { | |
p.Tunit = defunit | |
} | |
canvas.Start(p.Width, p.Height) | |
canvas.Rect(0, 0, p.Width, p.Height, "fill:"+p.Bgcolor) | |
canvas.Gstyle(fmt.Sprintf(globalstyle, p.Font, p.Tsize, p.Tunit, p.Tcolor)) | |
for _, s := range p.Sections { | |
// fmt.Fprintf(os.Stderr, "Section %d\n%v\n", i, s) | |
s.display(p.Width, p.Height, p.Inset, p.Tsize, p.Bgcolor, p.Tunit, s.Item, true) | |
} | |
canvas.Gend() | |
canvas.End() | |
} | |
// display draws a section, given its rectangular dimensions, expressed as float percentages | |
// the section struct is altered with the actual computed pixels | |
func (s *section) display(width, height, inset, tsize int, bgcolor, unit string, items []titem, showlabel bool) { | |
//fmt.Fprintf(os.Stderr, "\nIn Display items=%v\n", items) | |
s.dx = pct(width, s.X) | |
s.dy = pct(height, s.Y) | |
s.dw = pct(width, s.W) | |
s.dh = pct(height, s.H) | |
fs := 12 | |
if s.Bgcolor == "" { | |
s.Bgcolor = bgcolor | |
} | |
canvas.Rect(s.dx+inset, s.dy+inset, s.dw-(inset*2), s.dh-(inset*2), "fill:"+s.Bgcolor) | |
var tfs int | |
if len(s.Content) > 0 { | |
var pfs float64 | |
var align string | |
if s.Tsize == 0 { | |
s.Tsize = 100 | |
pfs = 100 | |
} else { | |
pfs = s.Tsize | |
} | |
if s.Align == "" { | |
align = "middle" | |
} else { | |
align = s.Align | |
} | |
tfs = pct(tsize, pfs) | |
s.textblock(tfs, inset, align, unit) | |
} | |
if len(items) > 0 { | |
list := []string{} | |
for _, x := range items { | |
list = append(list, x.Content) | |
} | |
textrect(s.dx+tfs, s.dy+tfs, s.dw-10, tfs, list, "lightgray", s.Tcolor) | |
// markerlist(s.dx+tfs, s.dy+tfs, markerglyph, list, tfs, tfs+4, s.Tcolor, "start") | |
} | |
if showlabel && len(s.Label) > 0 { | |
// vtext(s.dx+inset, s.dy+inset, 90, s.Label, stextstyle+s.Tcolor) | |
canvas.Text(s.dx+fs, (s.dy+s.dh)-10, s.Label, stextstyle+s.Tcolor) | |
} | |
} | |
// textblock draws a block of text within a section, wordwrapped | |
func (s *section) textblock(size, inset int, align, unit string) { | |
var x int | |
tfmt := "baseline-shift:-33%" | |
if align == "middle" { | |
x = s.dx + inset + (s.dw-inset*2)/2 | |
tfmt = "baseline-shift:-33%" | |
} else { | |
x = s.dx + inset + size | |
} | |
var wraplen int | |
wraplen = size + ((size * 2) / 3) | |
if wraplen < 20 { | |
wraplen = 20 | |
} | |
words := wordwrap(s.Content, wraplen, 1) | |
canvas.Gstyle(fmt.Sprintf("font-style:%s;fill:%s;text-anchor:%s;font-size:%.2f%%", s.Tstyle, s.Tcolor, align, s.Tsize)) | |
y := s.dy + inset + ((s.dh - (inset * 2)) / (len(words) + 1)) | |
spacing := size + size/2 | |
for i := 0; i < len(words); i++ { | |
canvas.Text(x, y, words[i], tfmt) | |
y += spacing | |
} | |
canvas.Gend() | |
} | |
var markerglyph = string(0x25b6) | |
// docontent fills in the content into a rectangular area: | |
// either a reference to content, or a plain or annontated list | |
func (sl slist) docontent(size, inset int) { | |
s := sl.section | |
spacing := size + 4 | |
if len(s.Href) > 0 { | |
subpic(s.dx, s.dy, s.Href) | |
} else { | |
list := []string{} | |
for _, x := range sl.Item { | |
list = append(list, x.Content) | |
} | |
markerlist(s.dx+inset, s.dy+inset, markerglyph, list, size, spacing, s.Tcolor, "start") | |
pmarker(sl, inset, spacing) | |
} | |
} | |
// pmarker shows a percentage marker | |
func pmarker(s slist, inset, spacing int) { | |
var bx, bl, x int | |
h := 15 | |
w := pct(s.section.dw, 20) // 120 | |
y := s.section.dy + spacing | |
for _, m := range s.Item { | |
if m.Start >= 0 && m.End > 0 { | |
x = (s.section.dx + s.section.dw) - w - 10 | |
bx = int((m.Start / 100) * float64(w)) | |
bl = int((m.End / 100) * float64(w)) | |
canvas.Rect(x, y, w, h, "fill:lightgray") | |
canvas.Roundrect(x+bx+2, y+2, bl-bx-4, h-4, 2, 2, "fill:rgb(128,0,0)") | |
} | |
y += spacing | |
} | |
} | |
// markerlist makes a list headed by a marker character. Long lines are word-wrapped | |
func markerlist(x, y int, marker string, list []string, size, spacing int, fill, align string) { | |
canvas.Gstyle(fmt.Sprintf("fill:%s;font-size:%dpx", fill, size)) | |
indent := size + 2 | |
for _, s := range list { | |
wlist := wordwrap(s, size*3, 1) | |
canvas.Text(x, y, marker, "fill:"+fill) | |
canvas.Text(x+indent, y, wlist[0]) | |
if len(wlist) > 1 { | |
for wi := 1; wi < len(wlist); wi++ { | |
y += spacing | |
canvas.Text(x+indent, y, wlist[wi]) | |
} | |
} | |
y += spacing | |
} | |
canvas.Gend() | |
} | |
func textrect(x, y, w, h int, text []string, rcolor, tcolor string) { | |
tx := x + 10 | |
ty := y + 10 | |
canvas.Textlines(x+tx, y+ty, text, 12, 14, "fill:"+tcolor, "start") | |
for i := 0; i < len(text); i++ { | |
canvas.Rect(x+tx, y, w, h, "stroke:red;fill:"+rcolor) | |
y += 16 | |
} | |
} | |
func textwidth(c uint8, px int) int { | |
return px | |
} | |
// wordrap word-wraps a single string into a string slice, whose members are no longer than | |
// the specified number of characters | |
func wordwrap(s string, maxWidth, px int) []string { | |
a := []string{} | |
w := 0 | |
i := 0 | |
rs := 0 | |
if maxWidth < 10 { | |
return a | |
} | |
for i < len(s) { | |
c := s[i] | |
w++ // w += textwidth(c, px) | |
if c == ' ' { | |
rs = i | |
} | |
if w > maxWidth { | |
sub := s[0:rs] | |
if len(sub) > 0 && sub[0] == ' ' { | |
sub = sub[1:len(sub)] | |
} | |
a = append(a, sub) | |
s = s[rs:len(s)] | |
i = 0 | |
w = 0 | |
} else { | |
i++ | |
} | |
} | |
if len(s) > 0 && s[0] == ' ' { | |
s = s[1:len(s)] | |
} | |
a = append(a, s) | |
return a | |
} | |
// subpic places a SVG file at a coordinate | |
func subpic(x, y int, picname string) { | |
var s SVG | |
f, err := os.Open(picname) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "%v\n", err) | |
return | |
} | |
defer f.Close() | |
if err := xml.NewDecoder(f).Decode(&s); err != nil { | |
fmt.Fprintf(os.Stderr, "Unable to parse (%v)\n", err) | |
return | |
} | |
canvas.Group(fmt.Sprintf(`clip-path="url(#%s)"`, picname), fmt.Sprintf(`transform="translate(%d,%d)"`, x, y)) | |
canvas.ClipPath(fmt.Sprintf(`id="%s"`, picname)) | |
canvas.Rect(0, 0, s.Width, s.Height) | |
canvas.ClipEnd() | |
io.WriteString(canvas.Writer, s.Doc) | |
canvas.Gend() | |
} | |
// vtext draws on an angle | |
func vtext(x, y int, angle float64, s string, style ...string) { | |
canvas.TranslateRotate(x, y, angle) | |
canvas.Text(0, 0, s, style...) | |
canvas.Gend() | |
} | |
// compute the pixels given a percentage and width | |
func pct(n int, p float64) int { | |
x := (p / 100) * float64(n) | |
i := int(x) | |
if x-float64(i) > 0.5 { | |
return i + 1 | |
} | |
return i | |
} | |
// make a pbox for every file specified on the command line | |
// there are no flags | |
func main() { | |
if len(os.Args) > 1 { | |
for i := 1; i < len(os.Args); i++ { | |
dopbox(os.Args[i]) | |
} | |
} else { | |
dopbox("") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment