diff --git a/README.md b/README.md index e2b0a9a..3ba444d 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,6 @@ You can also copy *.png files into the `examples` folder and include their names That will create corresponding *.html files. +## Binaries ## +The dist folder includes binaries for different operating systems. +They allow you to convert *.png files to svg by including the png file name(s) as command line arguments. \ No newline at end of file diff --git a/dist/darwin/386/pixel2svg b/dist/darwin/386/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/darwin/386/pixel2svg differ diff --git a/dist/darwin/amd64/pixel2svg b/dist/darwin/amd64/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/darwin/amd64/pixel2svg differ diff --git a/dist/freebsd/386/pixel2svg b/dist/freebsd/386/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/freebsd/386/pixel2svg differ diff --git a/dist/freebsd/amd64/pixel2svg b/dist/freebsd/amd64/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/freebsd/amd64/pixel2svg differ diff --git a/dist/freebsd/arm/pixel2svg b/dist/freebsd/arm/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/freebsd/arm/pixel2svg differ diff --git a/dist/linux/386/pixel2svg b/dist/linux/386/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/linux/386/pixel2svg differ diff --git a/dist/linux/amd64/pixel2svg b/dist/linux/amd64/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/linux/amd64/pixel2svg differ diff --git a/dist/linux/arm/pixel2svg b/dist/linux/arm/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/linux/arm/pixel2svg differ diff --git a/dist/netbsd/386/pixel2svg b/dist/netbsd/386/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/netbsd/386/pixel2svg differ diff --git a/dist/netbsd/amd64/pixel2svg b/dist/netbsd/amd64/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/netbsd/amd64/pixel2svg differ diff --git a/dist/netbsd/arm/pixel2svg b/dist/netbsd/arm/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/netbsd/arm/pixel2svg differ diff --git a/dist/openbsd/386/pixel2svg b/dist/openbsd/386/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/openbsd/386/pixel2svg differ diff --git a/dist/openbsd/amd64/pixel2svg b/dist/openbsd/amd64/pixel2svg new file mode 100644 index 0000000..159934b Binary files /dev/null and b/dist/openbsd/amd64/pixel2svg differ diff --git a/examples/main.go b/examples/main.go index aa6a52c..6fb3d94 100644 --- a/examples/main.go +++ b/examples/main.go @@ -79,22 +79,23 @@ func convertToUint8(rgbTone uint32) uint8 { return uint8(rgbTone / 0x101) } -func ReadPNGPixels(filePath string) [][][4]uint8 { +func ReadPNGPixels(filePath string) ([][][4]uint8, error) { + + var infile *os.File + var err error + var src image.Image + // fmt.Println("\nReading \n", filePath) - infile, err := os.Open(filePath) - if err != nil { - // replace this with good error handling - panic(err) + if infile, err = os.Open(filePath); err != nil { + return nil, err } defer infile.Close() // Decode will figure out what type of image is in the file on its own. // We just have to be sure all the image packages we want are imported. - src, _, err := image.Decode(infile) - if err != nil { - // replace this with good error handling - panic(err) + if src, _, err = image.Decode(infile); err != nil { + return nil, err } colorGrid := [][][4]uint8{} @@ -114,7 +115,14 @@ func ReadPNGPixels(filePath string) [][][4]uint8 { colorGrid = append(colorGrid, newCol) } - return colorGrid + return colorGrid, nil +} + +func addError(errors *[]string, summary string, err error) { + *errors = append( + *errors, + strings.Join([]string{summary, err.Error()}, " "), + ) } /* @@ -126,20 +134,41 @@ func ReadPNGPixels(filePath string) [][][4]uint8 { * That will create corresponding *.html files. */ func main() { + errors := []string{} var s pixels2svg.ShapeExtractor + var colorGrid [][][4]uint8 + var err error s.Init(sailboat()) s.WriteSVGToFile("example_sailboat.html") - s.Init(ReadPNGPixels("test1.png")) - s.WriteSVGToFile("example_test1.html") + if colorGrid, err = ReadPNGPixels("test1.png"); err == nil { + s.Init(colorGrid) + s.WriteSVGToFile("example_test1.html") + } args := os.Args[1:] + if len(args) <= 0 { + println("\n To create from other png files, just add their names as command line arguments.") + } + for _, nextInput := range args { if strings.HasSuffix(nextInput, ".png") { - s.Init(ReadPNGPixels(nextInput)) + colorGrid, err := ReadPNGPixels(nextInput) + if err != nil { + addError(&errors, strings.Join([]string{" Error: ", nextInput, " ... "}, ""), err) + continue + } + s.Init(colorGrid) newName := strings.TrimSuffix(nextInput, ".png") + ".html" s.WriteSVGToFile(newName) } } + + if len(errors) > 0 { + println("\nRan into error(s) ...") + for _, nextErr := range errors { + println(nextErr) + } + } } diff --git a/pixels2svg/pixels2svg.go b/pixels2svg/pixels2svg.go index 64544ea..1d224e6 100644 --- a/pixels2svg/pixels2svg.go +++ b/pixels2svg/pixels2svg.go @@ -28,6 +28,7 @@ type ShapeExtractor struct { ColCount int RowCount int neighborEvaluators [8]evaluatorFunc + cellQueue [][2]int } func (s *ShapeExtractor) showAlreadyDone() { @@ -215,10 +216,41 @@ func (s *ShapeExtractor) getLeftDirection(direction int) int { * get the direction slightly to its right. */ func (s *ShapeExtractor) getAngledRightDirection(direction int) int { - if direction >= 7 { + return (direction + 1) % 8 +} + +/* + * Assuming an outline walker is headed a certain direction, + * get the direction to the right (90 degrees). + */ +func (s *ShapeExtractor) getRight90Direction(direction int) int { + return (direction + 2) % 8 +} + +func (s *ShapeExtractor) getLatestDirection(colX1, rowY1, colX2, rowY2 int) int { + colDiff := colX2 - colX1 + rowDiff := rowY2 - rowY1 + + switch { + case (colDiff == 0 && rowDiff < 0): return 0 + case (colDiff > 0 && rowDiff < 0): + return 1 + case (colDiff > 0 && rowDiff == 0): + return 2 + case (colDiff > 0 && rowDiff > 0): + return 3 + case (colDiff == 0 && rowDiff > 0): + return 4 + case (colDiff < 0 && rowDiff > 0): + return 5 + case (colDiff < 0 && rowDiff == 0): + return 6 + case (colDiff < 0 && rowDiff < 0): + return 7 } - return direction + 1 + + return 0 } /* @@ -424,6 +456,43 @@ func (s *ShapeExtractor) GetPolygonsFromCell( return allPolygons } +/* + * Check the neighboring cells. If they are the + * same color and not yet done, mark them as done and add them to the queue. + */ +func (s *ShapeExtractor) addNeighborsToQueue(colX, rowY int, color [4]uint8) { + + for direction := 0; direction <= 7; direction += 1 { + + evaluator := s.neighborEvaluators[direction] + isGood := evaluator(colX, rowY, color) + + if isGood { + nextCol, nextRow := s.getCellInDirection(colX, rowY, direction) + s.alreadyDone[nextCol][nextRow] = true + s.cellQueue = append(s.cellQueue, [2]int{nextCol, nextRow}) + } + } +} + +/* + * For each cell in the queue, mark it as already done + * and remove it from the queue, but also + * add its neighbors to the queue if they have the same color + * and aren't done yet + */ +func (s *ShapeExtractor) markCellQueueDone(color [4]uint8) { + if len(s.cellQueue) < 1 { + return + } + + colX, rowY := split2Int(s.cellQueue[0]) + s.addNeighborsToQueue(colX, rowY, color) + s.cellQueue = s.cellQueue[1:] + + s.markCellQueueDone(color) +} + /* * Given a polygon with an outline starting at a certain cell, mark * all its points (outline and internal) as "alreadyDone". @@ -432,31 +501,61 @@ func (s *ShapeExtractor) GetPolygonsFromCell( * Assumes first point is the highest row of the outline and the * left-most cell of that row. * - * From each point on the outline of the polygon, marks it and all cells - * below it as already done until it runs into another cell of the outline - * or a cell of a different color + * Marks each point on the outline as already done and also adds the + * cell to their "right" and "angled right" to a queue for continuing + * the process of marking as done. * */ func (s *ShapeExtractor) markPolygonAlreadyDone(polygonOutline [][2]int) { - firstCol, firstRow := split2Int(polygonOutline[0]) - color := s.grid[firstCol][firstRow] - for _, nextPoint := range polygonOutline { + if len(polygonOutline) < 3 { + return + } + + s.setNeighborEvaluators() + s.cellQueue = [][2]int{} + + // deal with first cell on its own + prevCol, prevRow := split2Int(polygonOutline[0]) + color := s.grid[prevCol][prevRow] + s.alreadyDone[prevCol][prevRow] = true + + // Walk through outline and add cells to the right to the queue + // of cells to mark as already done + for _, nextPoint := range polygonOutline[1:] { nextCol, nextRow := split2Int(nextPoint) + direction := s.getLatestDirection(prevCol, prevRow, nextCol, nextRow) s.alreadyDone[nextCol][nextRow] = true - for lowerRow := nextRow + 1; lowerRow < s.RowCount; lowerRow++ { - if color != s.grid[nextCol][lowerRow] { - break - } + // Get its cell to the "right" and if necessary, add to the queue + innerDirection := s.getRight90Direction(direction) - if IsPointIn2IntArray(nextCol, lowerRow, polygonOutline) { - break - } + evaluator := s.neighborEvaluators[innerDirection] + isGood := evaluator(nextCol, nextRow, color) + if isGood { + innerCol, innerRow := s.getCellInDirection(nextCol, nextRow, innerDirection) + s.alreadyDone[innerCol][innerRow] = true - s.alreadyDone[nextCol][lowerRow] = true + s.cellQueue = append(s.cellQueue, [2]int{innerCol, innerRow}) } + + // Get its cell slightly to the "right" and if necessary, add to the queue + innerDirection = s.getAngledRightDirection(direction) + + evaluator = s.neighborEvaluators[innerDirection] + isGood = evaluator(nextCol, nextRow, color) + if isGood { + innerCol, innerRow := s.getCellInDirection(nextCol, nextRow, innerDirection) + s.alreadyDone[innerCol][innerRow] = true + + s.cellQueue = append(s.cellQueue, [2]int{innerCol, innerRow}) + } + + prevCol = nextCol + prevRow = nextRow } + + s.markCellQueueDone(color) // s.showAlreadyDone() // println("\n") } diff --git a/pixels2svg/pixels2svg_test.go b/pixels2svg/pixels2svg_test.go index 46acc0c..cf1867f 100644 --- a/pixels2svg/pixels2svg_test.go +++ b/pixels2svg/pixels2svg_test.go @@ -167,42 +167,37 @@ func compareSliceofStrings(results, expected []string) string { return "" } +/* + * Five columns, four rows - all almost black + */ func getColorGrid() [][][4]uint8 { + black := [4]uint8{1, 1, 1, 1} grid := [][][4]uint8{ - { // First column - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - }, - { // Second column - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - }, - { // Third column - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - }, - { // Fourth column - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - }, - { // Fifth column - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - {1, 1, 1, 1}, - }, + {black, black, black, black}, // Column 0 + {black, black, black, black}, // Column 1 + {black, black, black, black}, // Column 2 + {black, black, black, black}, // Column 3 + {black, black, black, black}, // Column 4 } return grid } +/* + * 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + * 0 r r r r r r y y y y y y g g g g g g + * 1 r r r r r r b y y y y y g g g g g g + * 2 r r r r r r b b y y y y g g g g g g + * 3 r r r r r r b b b y y y g g g g g g + * 4 r r r b r r b b b b y y g g r r g g + * 5 r r r b r r b b b b b y g g r r g g + * 6 r r r b r r b b b b b b g g r r g g + * 7 r r r b r r b b b b b b g g r r g g + * 8 r r r r r r b b b b b b g g g g g g + * 9 r r r r r r b b b b b b g g g g g g + * 10 r r r r r r b b b b b b g g g g g g + * 11 r r r r r r b b b b b b g g g g g g + * 12 + */ func getBigColorGrid() [][][4]uint8 { gridColors := [][][4]uint8{} @@ -522,6 +517,85 @@ func TestOutlinePolygonPartial(t *testing.T) { } } +func TestAddNeighborsToQueueTopEdge(t *testing.T) { + var s ShapeExtractor + + s.setNeighborEvaluators() + + gridColors := getBigColorGrid() + s.Init(gridColors) + s.cellQueue = [][2]int{} + red := s.grid[0][0] + + // outline and already done points + s.alreadyDone[2][0] = true + s.alreadyDone[3][0] = true + s.alreadyDone[4][0] = true + s.alreadyDone[2][1] = true + + s.addNeighborsToQueue(3, 1, red) + results := s.cellQueue + expected := [][2]int{{4, 1}, {4, 2}, {3, 2}, {2, 2}} + + err := compareOutlinePoints(results, expected) + if err != "" { + t.Errorf("Cell Queue. %s", err) + } +} + +func TestAddNeighborsToQueueTopRightCorner(t *testing.T) { + var s ShapeExtractor + + s.setNeighborEvaluators() + + gridColors := getBigColorGrid() + s.Init(gridColors) + s.cellQueue = [][2]int{} + red := s.grid[0][0] + + s.alreadyDone[3][0] = true + s.alreadyDone[4][0] = true + s.alreadyDone[5][0] = true + s.alreadyDone[5][1] = true + s.alreadyDone[5][2] = true + s.alreadyDone[3][1] = true + + s.addNeighborsToQueue(4, 1, red) // (5, 0) and (5, 1) are on outline + results := s.cellQueue + expected := [][2]int{{4, 2}, {3, 2}} + + err := compareOutlinePoints(results, expected) + if err != "" { + t.Errorf("Cell Queue. %s", err) + } +} + +func TestAddNeighborsToQueueByInnerDiff(t *testing.T) { + var s ShapeExtractor + + s.setNeighborEvaluators() + + gridColors := getBigColorGrid() + s.Init(gridColors) + s.cellQueue = [][2]int{} + red := s.grid[0][0] + + s.alreadyDone[2][2] = true + s.alreadyDone[3][2] = true + s.alreadyDone[4][2] = true + s.alreadyDone[5][2] = true + s.alreadyDone[5][3] = true + + s.addNeighborsToQueue(3, 3, red) + results := s.cellQueue + expected := [][2]int{{4, 3}, {4, 4}, {2, 4}, {2, 3}} + + err := compareOutlinePoints(results, expected) + if err != "" { + t.Errorf("Cell Queue. %s", err) + } +} + /* * | X | X | X | X | X | X | * | X | 0 | 1 | X | 3 | 4 | @@ -584,9 +658,9 @@ func TestMarkPolygonAlreadyDonePartial(t *testing.T) { results := s.alreadyDone expected := [][]bool{ - {false, false, true, true, true, true}, // column 0 - {false, true, true, false, false, true}, // column 1 - {false, true, true, false, false, true}, + {false, false, true, true, true, true}, // column 0 + {false, true, true, false, true, true}, // column 1 + {false, true, true, false, true, true}, {false, false, true, true, true, true}, {false, true, true, true, true, true}, {false, true, true, true, true, true}, @@ -1066,9 +1140,7 @@ func TestGetSVGTextLarge(t *testing.T) { - - `