1package authoring23import (4 "bufio"5 "bytes"6 "fmt"7 "image"8 "image/color"9 _ "image/gif"10 _ "image/jpeg"11 "image/png"12 "os"1314 "github.com/FelineStateMachine/atlas/format/vnext"15)1617func deriveRasterFile(source Raster, capture Capture, cache *Cache) (vnext.RasterPyramid, []vnext.Blob, error) {18 data, err := os.ReadFile(cache.BlobPath(capture.Body))19 if err != nil {20 return vnext.RasterPyramid{}, nil, fmt.Errorf("read raster %s: %w", source.ID, err)21 }22 decoded, err := decodeRaster(data)23 if err != nil {24 return vnext.RasterPyramid{}, nil, fmt.Errorf("decode raster %s: %w", source.ID, err)25 }26 formats := make([]string, source.MaxZoom+1)27 for index := range formats {28 formats[index] = "png"29 }30 fullZoom := source.FullZoom31 if fullZoom == 0 {32 fullZoom = source.MaxZoom33 }34 pyramid := vnext.RasterPyramid{35 ID: "sample", Name: source.Name, Codec: "image/png", TileSize: source.TileSize,36 MinZoom: 0, MaxZoom: source.MaxZoom, FullZoom: fullZoom, SourceZoom: source.SourceZoom,37 Template: "tiles/" + source.ID + "/{z}/{x}/{y}.{format}", Formats: formats,38 Bounds: nativeRect(source.Bounds), Surface: nativeRect(source.Surface),39 Interpolate: source.Interpolate, Background: source.Background, Shard: source.Shard,40 }41 var blobs []vnext.Blob42 for zoom := int64(0); zoom <= source.MaxZoom; zoom++ {43 tiles := int64(1) << zoom44 for y := int64(0); y < tiles; y++ {45 for x := int64(0); x < tiles; x++ {46 data, err := renderRasterTile(decoded, source.TileSize, zoom, x, y)47 if err != nil {48 return vnext.RasterPyramid{}, nil, err49 }50 blobs = append(blobs, vnext.Blob{51 Name: fmt.Sprintf("tiles/%s/%d/%d/%d.png", source.ID, zoom, x, y), Data: data,52 })53 }54 }55 }56 return pyramid, blobs, nil57}5859func decodeRaster(data []byte) (image.Image, error) {60 decoded, _, err := image.Decode(bytes.NewReader(data))61 if err == nil {62 return decoded, nil63 }64 return decodeP3(data)65}6667func decodeP3(data []byte) (image.Image, error) {68 reader := bufio.NewReader(bytes.NewReader(data))69 var magic string70 var width, height, maximum int71 if _, err := fmt.Fscan(reader, &magic, &width, &height, &maximum); err != nil {72 return nil, err73 }74 if magic != "P3" || width <= 0 || height <= 0 || maximum <= 0 || maximum > 65535 {75 return nil, fmt.Errorf("unsupported raster encoding")76 }77 out := image.NewNRGBA(image.Rect(0, 0, width, height))78 for y := 0; y < height; y++ {79 for x := 0; x < width; x++ {80 var red, green, blue int81 if _, err := fmt.Fscan(reader, &red, &green, &blue); err != nil {82 return nil, fmt.Errorf("read pixel %d,%d: %w", x, y, err)83 }84 if red < 0 || red > maximum || green < 0 || green > maximum || blue < 0 || blue > maximum {85 return nil, fmt.Errorf("pixel %d,%d exceeds raster range", x, y)86 }87 out.SetNRGBA(x, y, color.NRGBA{88 R: uint8(red * 255 / maximum), G: uint8(green * 255 / maximum), B: uint8(blue * 255 / maximum), A: 255,89 })90 }91 }92 return out, nil93}9495func renderRasterTile(source image.Image, tileSize, zoom, tileX, tileY int64) ([]byte, error) {96 tile := image.NewNRGBA(image.Rect(0, 0, int(tileSize), int(tileSize)))97 bounds := source.Bounds()98 span := tileSize * (int64(1) << zoom)99 for y := int64(0); y < tileSize; y++ {100 for x := int64(0); x < tileSize; x++ {101 sourceX := bounds.Min.X + int((tileX*tileSize+x)*int64(bounds.Dx())/span)102 sourceY := bounds.Min.Y + int((tileY*tileSize+y)*int64(bounds.Dy())/span)103 if sourceX >= bounds.Max.X {104 sourceX = bounds.Max.X - 1105 }106 if sourceY >= bounds.Max.Y {107 sourceY = bounds.Max.Y - 1108 }109 tile.Set(int(x), int(y), source.At(sourceX, sourceY))110 }111 }112 var out bytes.Buffer113 encoder := png.Encoder{CompressionLevel: png.BestCompression}114 if err := encoder.Encode(&out, tile); err != nil {115 return nil, fmt.Errorf("encode canonical raster tile: %w", err)116 }117 return out.Bytes(), nil118}119120func nativeRect(source *RasterRect) *vnext.RasterRect {121 if source == nil {122 return nil123 }124 return &vnext.RasterRect{X: source.X, Y: source.Y, Width: source.Width, Height: source.Height}125}