-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathImageProcessor.go
643 lines (519 loc) · 19.5 KB
/
ImageProcessor.go
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
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
// reads works data from localhost/api/v1/works.xml and produces a set of static html files to navigate images.
package main
// the import statement makes sure all the required packages to run this program are included
import (
"encoding/xml"
"fmt"
"html"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
)
func main() {
fmt.Println("Image processor starting...")
// expecting two command-line arguments at invocation - API location for reading image data from and output directory for writing static site files
if len(os.Args) <= 2 {
fmt.Println("Error: please enter the image API URL and an output directory location as command-line arguments (e.g. >go run ImageProcessor http://localhost/test/api/v1/works.xml code/html/output)")
return
}
// read in command line arguments: API URL and output directory
imageAPILocation := os.Args[1]
outputFolderLocation := os.Args[2]
fmt.Printf("Accessing image API at %s\n", imageAPILocation)
fmt.Printf("Output files for static site will be written to <./%s>\n", outputFolderLocation)
// get XML data response from API location
apiDataResp, err := http.Get(imageAPILocation)
if err != nil {
fmt.Fprintf(os.Stderr, "Error fetching XML works data from specified API URL (%s): %v\n", imageAPILocation, err)
os.Exit(1)
}
// decode read XML data body
dec := xml.NewDecoder(apiDataResp.Body)
// predefine the token literal values we're interested in for ease of comparison when processing the XML data body (e.g. <id>, <filename>, <work> etc)
ID := "id"
FILENAME := "filename"
WORK := "work"
MODEL := "model"
MAKE := "make"
URISMALL := "small"
URIMEDIUM := "medium"
URILARGE := "large"
// drclare in-memory collections for works and makes, and a stack to read in XML tag tokens
var stack []string // we'll use a string slice as a stack data structure to pop on/off start/end elements as we read through the XML data body's tokens
var works []*Work // collection of all works detected
var makes []*Make // collection of all makes detected
var worksSM []*Work // Works sans makes - if a work is found without a make speceifed, it'll go on this list and have a separate page generated for it to be diplayed
var newWork *Work // placeholder for the work currently being iterated through
newModelDetected := false // flag indicating if a new model was detected in the work being currently read (if so to be added to the make of this work)
newModel := "" // name of new model detected (if indicated by newModelDetected flag above)
// URI data type flags - these will be set when the XML data is read, so the contained data canbe assigned to the correct attribute ofthe in-memory works object we're building
thumbnailURI := false
mediumURI := false
largeURI := false
// iterate through the full decoded XML data body per detected token, until EOF
// we'll detect three types of main tokens: start tags, end tags and data - tags will be popped on to the stack when they open and popped off when closing.
// depending on the current opened token, we'll read in the data to the current in-memory work object (if we're interested in that data)
for {
// get next XML token
token, err := dec.Token()
// handle any errors
if err == io.EOF {
// reached end of data
break
} else if err != nil {
fmt.Fprintf(os.Stderr, "Error reading XML data body token: %v\n", err)
os.Exit(1)
}
// switch statement to take selective action based on the current token (start, end or data)
switch token := token.(type) {
case xml.StartElement:
// XML start element: push on stack and initialize new work object
stack = append(stack, token.Name.Local)
if len(stack) > 0 && stack[len(stack)-1] == WORK {
// start of a new <work> in XML data: create a new Work instance and pop in to the list of all works
newWork = createWork()
works = append(works, newWork)
}
// if we're reading the URL tag of a work, set the appropriate flag depending on the the small, medium or large XML tag attribute
if len(stack) > 0 && stack[len(stack)-1] == "url" {
for _, val := range token.Attr {
if val.Value == URISMALL {
thumbnailURI = true
}
if val.Value == URIMEDIUM {
mediumURI = true
}
if val.Value == URILARGE {
largeURI = true
}
}
}
case xml.EndElement:
// XML end element: pop off stack, and finalize current in-memory work object
// check if there are already XML opening tags stored in stack - if not, we've encountered a closing tag without an opening tag
if len(stack) <= 0 {
fmt.Fprintf(os.Stderr, "Attempting to pop an element(%s) without any on stack - possibly malformed XML\n", token.Name.Local)
os.Exit(1)
}
elementPopped := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// check for XML consistency - if every end element should have had a corresponding start element
if elementPopped != token.Name.Local {
fmt.Fprintf(os.Stderr, "Closing element %s without matching opener (%s) - possibly malformed XML\n", elementPopped, token.Name.Local)
os.Exit(1)
}
if elementPopped == WORK {
// end of a <work> element (</work>)
// add this work to its make and model's works lists
thisWork := newWork
thisMake := newWork.WMake
thisModel := newWork.WModel
if thisMake == nil {
// record works without a make specified separately
worksSM = append(worksSM, thisWork)
}
// add this work to the works lists of its make and model - makes things easier when generating make and model pages
if thisMake != nil && thisModel != nil {
thisMake.Works = append(thisMake.Works, thisWork)
thisModel.Works = append(thisModel.Works, thisWork)
}
// reset running reference to the completed work, as well as other running placeholders - ready for a new work to start
newWork = nil
newModelDetected = false
newModel = ""
thisWork, thisMake, thisModel = nil, nil, nil
}
case xml.CharData:
// XML data - populate the current work object based on XML data token (e.g. ID, model, make etc.)
//Work ID
if len(stack) > 0 && stack[len(stack)-1] == ID {
IDData, err := strconv.Atoi(strings.TrimSpace(string(token)))
if err != nil {
fmt.Fprintf(os.Stderr, "Error converting Work ID: %v\n", err)
os.Exit(1)
}
if newWork != nil {
newWork.ID = IDData
} else {
fmt.Fprintf(os.Stderr, "ID data(%d) detected without an active current Work struct instance. Possibly malformed XML.", IDData)
os.Exit(1)
}
}
// Work filename
if len(stack) > 0 && stack[len(stack)-1] == FILENAME {
FileName := strings.TrimSpace(string(token))
if newWork != nil {
newWork.FileName = FileName
} else {
fmt.Fprintf(os.Stderr, "Filename(%s) detected without an active current Work struct instance. Possibly malformed XML.", FileName)
os.Exit(1)
}
}
// Work camera make
if len(stack) > 0 && stack[len(stack)-1] == MAKE {
// make detected: retrieve make if already recorded, create if new
thisToken := strings.TrimSpace(string(token))
var thisMake *Make
if newWork != nil {
if thisToken == "" {
thisToken = "(Generic make)"
}
// check if this make is recorded in the global makes list
makeFound := false
for _, make := range makes {
if make != nil && make.Name == thisToken {
thisMake = make
makeFound = true
break
}
}
if makeFound {
// known make, populate work's make attribute with the make from the global list
works[len(works)-1].WMake = thisMake
} else {
// new make: create and add to global makes list, and populate this work's make attribute with it
thisMake = createMake(thisToken)
makes = append(makes, thisMake)
works[len(works)-1].WMake = thisMake
}
} else {
fmt.Fprintf(os.Stderr, "Make (%s) detected with no active Work element - possibly malformed XML input", thisToken)
os.Exit(1)
}
// camera model detected for this work?
if newModelDetected == true {
var thisModel *Model
var thisWork *Work
if len(works) > 0 {
thisWork = works[len(works)-1]
} else {
fmt.Fprintf(os.Stderr, "No works recorded, but already processing a camera model - malformed XML?")
os.Exit(1)
}
// if the model name is empty
if newModel == "" {
newModel = "(Generic model)"
}
// check if this model is recorded in this make's model list
modelFound := false
if thisWork.WMake != nil {
for _, model := range thisWork.WMake.Models {
if model != nil && model.Name == newModel {
thisModel = model
modelFound = true
}
}
}
if modelFound {
thisWork.WModel = thisModel
} else {
thisModel = createModel(newModel, thisMake)
if thisWork.WMake != nil {
thisWork.WMake.Models = append(thisWork.WMake.Models, thisModel)
thisWork.WModel = thisModel
}
thisModel = nil
newModel = ""
}
newModelDetected = false
}
}
if len(stack) > 0 && stack[len(stack)-1] == MODEL {
// model detected: add this model to the make.[]model of this work (if not already recorded)
thisToken := strings.TrimSpace(string(token))
newModel = thisToken
newModelDetected = true
}
if thumbnailURI == true {
// fmt.Println(strings.TrimSpace(string(token)))
if len(works) > 0 {
works[len(works)-1].URISmall = strings.TrimSpace(string(token))
}
thumbnailURI = false
}
if mediumURI == true {
if len(works) > 0 {
works[len(works)-1].URIMedium = strings.TrimSpace(string(token))
}
mediumURI = false
}
if largeURI == true {
if len(works) > 0 {
works[len(works)-1].URILarge = strings.TrimSpace(string(token))
}
largeURI = false
}
}
}
fmt.Println("XML data parsing complete - generating static site...")
// ------- Generate index.html -------------------
// check if the specified output directory exists - if not, create it
fileInPlace, e := fileExists("./" + outputFolderLocation)
if e != nil {
fmt.Fprintf(os.Stderr, "Error checking output directory placement: %v\n", e)
os.Exit(1)
}
if fileInPlace {
fmt.Println("Output directory for static site files (./" + outputFolderLocation + ") exists - files within with similar names will be overwritten.")
} else {
fmt.Println("Output directory for static site files (./" + outputFolderLocation + ") doesn't exist - creating..")
os.MkdirAll("./"+outputFolderLocation, 0755)
}
// open output file for writing
outFileName := "./" + outputFolderLocation + "/index.html"
f, err := os.Create(outFileName)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating index HTML file: %v\n", err)
os.Exit(1)
}
defer f.Close()
// dropdown navigation to all camera makes
indexNavigation := `<select onchange="if (this.value) window.location.href=this.value"><option value="">-- select a camera make</option>`
for _, mk := range makes {
if mk != nil {
indexNavigation = indexNavigation + `<option value="` + html.EscapeString(mk.PageURL) + `.html">` + html.EscapeString(mk.Name) + `</option>`
}
}
// if any works with no makes are recorded, add a generic option in navigation to access the page for those
if len(worksSM) > 0 {
indexNavigation = indexNavigation + `<option value="nomake.html">(no make/generic)</option></select>`
} else {
indexNavigation = indexNavigation + `</select>`
}
// create thumbnails of first 10 works
indexContent := "" // holding variable for image HTML
imgCount := 0 // counter to ensure first 10 image works are shown at most on index page
// create and append HTML to display each work (up to first ten)
for _, wk := range works {
indexContent = indexContent + `<img src=` + html.EscapeString(wk.URISmall) + `> `
imgCount++
if imgCount >= 10 {
break
}
}
// write the HTML structure of the index page containing navigation and image HTML to outout page
_, err = f.WriteString(`<!DOCTYPE html><html><head><title>Welcome to Phoots!</title><style type="text/css">nav { margin: 10px; }</style></head><body><header><h1>Welcome to Photos!</h1><nav>` + indexNavigation + `</nav></header>` + indexContent + `</body></html>`)
if err != nil {
fmt.Fprintf(os.Stderr, "Error writing output to disk file: %v\n", err)
os.Exit(1)
}
f.Sync()
// ------------- Generate individual pages for each of the camera makes ------------------
// for each make recorded
for _, mk := range makes {
if mk != nil {
// create HTML file
outFileName := "./" + outputFolderLocation + "/" + mk.PageURL + ".html"
f, err := os.Create(outFileName)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating camera make page: %v\n", err)
os.Exit(1)
}
defer f.Close()
// dropdown navigation to all camera models of this make
modelNavigation := `<select onchange="if (this.value) window.location.href=this.value"><option value="">-- select a camera model</option>`
for _, md := range mk.Models {
if md != nil {
modelNavigation = modelNavigation + `<option value="` + html.EscapeString(md.PageURL) + `.html">` + html.EscapeString(md.Name) + `</option>`
}
}
modelNavigation = modelNavigation + `</select>`
// create thumbnail HTML for first 10 works by this make
makeContent := ""
imgCount := 0
for _, wk := range mk.Works {
if wk != nil && wk.WMake != nil && wk.WMake.Name == mk.Name {
makeContent = makeContent + `<img src=` + html.EscapeString(wk.URISmall) + `> `
imgCount++
if imgCount >= 10 {
break
}
}
}
// write the make HTML page's output to file
_, err = f.WriteString(`<!DOCTYPE html><html><head><title>All photos taken with a ` + html.EscapeString(mk.Name) + `</title><style type="text/css">nav { margin: 10px; }</style></head><body><header><h1>All photos taken with a <i>` + mk.Name + `</i> camera</h1><nav><a href="index.html">back to homepage</a> | ` + modelNavigation + `</nav></header>` + makeContent + `</body></html>`)
if err != nil {
fmt.Fprintf(os.Stderr, "Error writing output to make HTML file: %v\n", err)
os.Exit(1)
}
f.Sync()
}
}
// ------------- Generate separate page for works without a make ------------------
// for each work recorded without a camera make
if len(worksSM) > 0 {
for _, wk := range worksSM {
if wk != nil {
outFileName := "./" + outputFolderLocation + "/nomake.html"
f, err := os.Create(outFileName)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating generic works page: %v\n", err)
os.Exit(1)
}
defer f.Close()
// create image thumbnails
genericContent := ""
if wk != nil {
genericContent = genericContent + `<img src=` + html.EscapeString(wk.URISmall) + `> `
}
// write to output file
_, err = f.WriteString(`<!DOCTYPE html><html><head><title>Generic Photographic Works</title><style type="text/css">nav { margin: 10px; }</style></head><body><header><h1>Generic Photos</h1><nav><a href="index.html">back to homepage</a> </nav></header>` + genericContent + `</body></html>`)
if err != nil {
fmt.Fprintf(os.Stderr, "Error writing output to generic make works file: %v\n", err)
os.Exit(1)
}
f.Sync()
}
}
}
// ------------- Generate individual pages for each of the camera models ------------------
// for each make
for _, mk := range makes {
if mk != nil {
// for each model of this make
for _, md := range mk.Models {
if md != nil {
// create HTML file
outFileName := "./" + outputFolderLocation + "/" + md.PageURL + ".html"
f, err := os.Create(outFileName)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating camera model page: %v\n", err)
os.Exit(1)
}
defer f.Close()
// create thumbnail HTML of first 10 works by this model
modelContent := ""
imgCount := 0
for _, wk := range md.Works {
if wk != nil && wk.WMake != nil && wk.WMake.Name == mk.Name {
modelContent = modelContent + `<img src=` + html.EscapeString(wk.URISmall) + `> `
imgCount++
if imgCount >= 10 {
break
}
}
}
// write HTML content to output file
_, err = f.WriteString(`<!DOCTYPE html><html><head><title>All photos taken with a ` + html.EscapeString(md.Name) + `</title><style type="text/css">nav { margin: 10px; }</style></head><body><header><h1>All photos taken with a <i>` + html.EscapeString(md.Name) + `</i> camera</h1><nav><a href="index.html">back to homepage</a> | <a href="` + html.EscapeString(mk.PageURL) + `.html">back to make</a></nav></header>` + modelContent + `</body></html>`)
}
}
}
}
fmt.Println("Static site generation complete.")
}
//----------------- custom data types -------------------------------
// type struct representing a photographic work
type Work struct {
ID int
FileName string
WMake *Make
WModel *Model
URISmall string
URIMedium string
URILarge string
}
// type struct representing a camera make
type Make struct {
ID int
Name string
Models []*Model
Works []*Work
PageURL string
}
// type struct representing a camera model
type Model struct {
ID int
MMake *Make
Works []*Work
Name string
PageURL string
}
//---------generator functions to create and return references to Works/Makes/Models ----------
// create and return a pointer to a make with a given string name
func createMake(name string) *Make {
var m Make
m.Name = name
// create the HTML filename for this make by stripping make name of all non-alphanumerics
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating make HTML filename: %v\n", err)
os.Exit(1)
}
m.PageURL = reg.ReplaceAllString(name, "-")
return &m
}
// create and return a pointer to a model with a given string name and make
func createModel(name string, make *Make) *Model {
var m Model
m.Name = name
m.MMake = make
// create the HTML filename for this model by stripping model name of all non-alphanumerics
reg, err := regexp.Compile("[^A-Za-z0-9]+")
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating model HTML filename: %v\n", err)
os.Exit(1)
}
m.PageURL = reg.ReplaceAllString(name, "-")
return &m
}
// create and return a pointer to a work
func createWork() *Work {
var w Work
w.ID = -1
w.FileName = ""
w.WMake = nil
w.WModel = nil
return &w
}
//----------------- Utility functions -------------------------------
// print a human-readable string description of a given Model struct instance (for debugging purposes)
func printMake(m *Make) {
if m == nil {
fmt.Println("<Invalid Make object>")
return
}
fmt.Println(m.Name + "(" + strconv.Itoa(len(m.Models)) + ")")
for _, model := range m.Models {
fmt.Println("\t" + model.Name)
}
return
}
// print a human-readable string description of a given Work struct instance (for debugging purposes)
func printWork(w *Work) {
if w == nil {
fmt.Println("<Invalid work object>")
return
}
wMakeName := ""
if w.WMake == nil {
wMakeName = "<Generic/undefined>"
} else {
wMakeName = w.WMake.Name
}
wModelName := ""
if w.WModel == nil {
wModelName = "<Generic/undefined>"
} else {
wModelName = w.WModel.Name
}
fmt.Println("[" + strconv.Itoa(w.ID) + "| " + wMakeName + "| " + wModelName + "]")
fmt.Println("\t Thumbnail: " + w.URISmall)
fmt.Println("\t Medium: " + w.URIMedium)
fmt.Println("\t Large: " + w.URILarge)
return
}
// returns a boolean flag indicating whether the given file or directory exists or not, along with an error that may have occured while checking
func fileExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return true, err
}
//!-