A tool for archiving & converting scans of postcards, and information about them.

feat: unpolished svg web format (no decode)

+261 -6
+2 -2
TODO.md
··· 1 1 # TODO 2 2 3 - - [ ] JPEGli in SVG with mask smaller than WebP? 3 + - [ ] Coregistration of front and back (slight rotation & translation differences) 4 + - [x] JPEGli in SVG with mask smaller than PNG? 4 5 - [ ] Improve HasTransparency detection (for all format decoding) 5 6 - [ ] Move component/decode.go#decodeImage to internal/images/decode.go? 6 7 - [ ] Fix XMP inject/extract tests ··· 11 12 - [ ] Store number of sides in the Metadata 12 13 - [ ] Validate the number of sides in the metadata with the number of images provided 13 14 - [ ] Allow conversion of postcard files on PostOffice 14 - - [ ] Coregistration of front and back (slight rotation & translation differences) 15 15 - [ ] Allow uploading/choice of SVGs for the back side, to make a "blank" postcard 16 16 - [ ] Responsive layout for the back — in physical units 17 17 - [ ] Rendering SVG inside go/wasm
+3 -1
formats.go
··· 21 21 var codecs = map[string]formats.Codec{ 22 22 "component": component.Codec(), 23 23 "web": web.DefaultCodec, 24 + "svg": web.SVGCodec, 24 25 "usd": usd.Codec(), 25 26 "usdz": usdz.Codec(), 26 27 "json": metadata.Codec(metadata.AsJSON), ··· 28 29 "xmp": xmp.Codec(), 29 30 } 30 31 31 - var Codecs = []string{"component", "web", "usdz", "usd", "json", "yaml", "xmp"} 32 + // Used for ordering 33 + var Codecs = []string{"component", "web", "svg", "usdz", "usd", "json", "yaml", "xmp"} 32 34 33 35 // These 'formats' will trigger the IncludeSupportFiles encoder option instead of a different codec 34 36 var supportFiles = []string{"css", "html"}
+1 -1
formats/usd/codec.go
··· 159 159 maxX, maxY := pc.Meta.Physical.FrontDimensions.MustPhysical() 160 160 161 161 // TODO: Coregister front & back? 162 - // TODO: Handle no back 163 162 164 163 frontPoints, err := images.Outline(pc.Front, false, true) 165 164 if err != nil { ··· 167 166 } 168 167 fTris := geom3d.Triangulate(frontPoints) 169 168 169 + // TODO: Handle no back 170 170 backPoints, err := images.Outline(pc.Back, true, true) 171 171 if err != nil { 172 172 return fmt.Errorf("back image can't be outlined: %w", err)
+2
formats/web/codec.go
··· 57 57 "jpeg": {}, 58 58 "webp": {lossless: true, transparency: true}, 59 59 "png": {lossless: true, transparency: true}, 60 + "svg": {transparency: true}, 60 61 } 61 62 62 63 // Only returns true if the capabilities on struct owning this method meet the needs of the provided capabilities object. ··· 78 79 } 79 80 80 81 var DefaultCodec, _ = Codec("jpeg", "webp") 82 + var SVGCodec, _ = Codec("svg") 81 83 82 84 func (c codec) Name() string { return codecName }
+2
formats/web/encode.go
··· 90 90 err = images.WritePNG(w, combinedImg, xmpData, opts.Archival) 91 91 case "jpeg": 92 92 err = images.WriteJPEG(w, combinedImg, xmpData) 93 + case "svg": 94 + err = writeSVG(w, pc, combinedImg, xmpData) 93 95 default: 94 96 err = fmt.Errorf("unsupported output image format: %s", format) 95 97 }
+31
formats/web/postcard.svg.qtpl
··· 1 + {% func SVG(v svgVars) %} 2 + <svg 3 + xmlns="http://www.w3.org/2000/svg" 4 + {% if v.size.HasPhysical() %} 5 + {% code 6 + cmW, cmH := v.size.MustPhysical() 7 + %} 8 + width="{%f.2 cmW %}cm" 9 + height="{%f.2 cmH * 2 %}cm" 10 + {% endif %} 11 + viewBox="0 0 {%d v.size.PxWidth %} {%d v.size.PxHeight * 2 %}" 12 + version="1.1" 13 + > 14 + <metadata>{%s= string(v.metadata) %}</metadata> 15 + <defs> 16 + <clipPath id="edge"> 17 + <path d="{%s pointsToPath(v.frontPoints, v.size.PxWidth, v.size.PxHeight, false) %}"/> 18 + <path d="{%s pointsToPath(v.backPoints, v.size.PxWidth, v.size.PxHeight, true) %}"/> 19 + </clipPath> 20 + </defs> 21 + <image 22 + x="0" 23 + y="0" 24 + width="100%" 25 + height="100%" 26 + preserveAspectRatio="none" 27 + clip-path="url(#edge)" 28 + href="data:image/jpeg;base64,{%s v.b64Img %}" 29 + /> 30 + </svg> 31 + {% endfunc %}
+124
formats/web/postcard.svg.qtpl.go
··· 1 + // Code generated by qtc from "postcard.svg.qtpl". DO NOT EDIT. 2 + // See https://github.com/valyala/quicktemplate for details. 3 + 4 + //line postcard.svg.qtpl:1 5 + package web 6 + 7 + //line postcard.svg.qtpl:1 8 + import ( 9 + qtio422016 "io" 10 + 11 + qt422016 "github.com/valyala/quicktemplate" 12 + ) 13 + 14 + //line postcard.svg.qtpl:1 15 + var ( 16 + _ = qtio422016.Copy 17 + _ = qt422016.AcquireByteBuffer 18 + ) 19 + 20 + //line postcard.svg.qtpl:1 21 + func StreamSVG(qw422016 *qt422016.Writer, v svgVars) { 22 + //line postcard.svg.qtpl:1 23 + qw422016.N().S(` 24 + <svg 25 + xmlns="http://www.w3.org/2000/svg" 26 + `) 27 + //line postcard.svg.qtpl:4 28 + if v.size.HasPhysical() { 29 + //line postcard.svg.qtpl:4 30 + qw422016.N().S(` 31 + `) 32 + //line postcard.svg.qtpl:6 33 + cmW, cmH := v.size.MustPhysical() 34 + 35 + //line postcard.svg.qtpl:7 36 + qw422016.N().S(` 37 + width="`) 38 + //line postcard.svg.qtpl:8 39 + qw422016.N().FPrec(cmW, 2) 40 + //line postcard.svg.qtpl:8 41 + qw422016.N().S(`cm" 42 + height="`) 43 + //line postcard.svg.qtpl:9 44 + qw422016.N().FPrec(cmH*2, 2) 45 + //line postcard.svg.qtpl:9 46 + qw422016.N().S(`cm" 47 + `) 48 + //line postcard.svg.qtpl:10 49 + } 50 + //line postcard.svg.qtpl:10 51 + qw422016.N().S(` 52 + viewBox="0 0 `) 53 + //line postcard.svg.qtpl:11 54 + qw422016.N().D(v.size.PxWidth) 55 + //line postcard.svg.qtpl:11 56 + qw422016.N().S(` `) 57 + //line postcard.svg.qtpl:11 58 + qw422016.N().D(v.size.PxHeight * 2) 59 + //line postcard.svg.qtpl:11 60 + qw422016.N().S(`" 61 + version="1.1" 62 + > 63 + <metadata>`) 64 + //line postcard.svg.qtpl:14 65 + qw422016.N().S(string(v.metadata)) 66 + //line postcard.svg.qtpl:14 67 + qw422016.N().S(`</metadata> 68 + <defs> 69 + <clipPath id="edge"> 70 + <path d="`) 71 + //line postcard.svg.qtpl:17 72 + qw422016.E().S(pointsToPath(v.frontPoints, v.size.PxWidth, v.size.PxHeight, false)) 73 + //line postcard.svg.qtpl:17 74 + qw422016.N().S(`"/> 75 + <path d="`) 76 + //line postcard.svg.qtpl:18 77 + qw422016.E().S(pointsToPath(v.backPoints, v.size.PxWidth, v.size.PxHeight, true)) 78 + //line postcard.svg.qtpl:18 79 + qw422016.N().S(`"/> 80 + </clipPath> 81 + </defs> 82 + <image 83 + x="0" 84 + y="0" 85 + width="100%" 86 + height="100%" 87 + preserveAspectRatio="none" 88 + clip-path="url(#edge)" 89 + href="data:image/jpeg;base64,`) 90 + //line postcard.svg.qtpl:28 91 + qw422016.E().S(v.b64Img) 92 + //line postcard.svg.qtpl:28 93 + qw422016.N().S(`" 94 + /> 95 + </svg> 96 + `) 97 + //line postcard.svg.qtpl:31 98 + } 99 + 100 + //line postcard.svg.qtpl:31 101 + func WriteSVG(qq422016 qtio422016.Writer, v svgVars) { 102 + //line postcard.svg.qtpl:31 103 + qw422016 := qt422016.AcquireWriter(qq422016) 104 + //line postcard.svg.qtpl:31 105 + StreamSVG(qw422016, v) 106 + //line postcard.svg.qtpl:31 107 + qt422016.ReleaseWriter(qw422016) 108 + //line postcard.svg.qtpl:31 109 + } 110 + 111 + //line postcard.svg.qtpl:31 112 + func SVG(v svgVars) string { 113 + //line postcard.svg.qtpl:31 114 + qb422016 := qt422016.AcquireByteBuffer() 115 + //line postcard.svg.qtpl:31 116 + WriteSVG(qb422016, v) 117 + //line postcard.svg.qtpl:31 118 + qs422016 := string(qb422016.B) 119 + //line postcard.svg.qtpl:31 120 + qt422016.ReleaseByteBuffer(qb422016) 121 + //line postcard.svg.qtpl:31 122 + return qs422016 123 + //line postcard.svg.qtpl:31 124 + }
+82
formats/web/svg.go
··· 1 + package web 2 + 3 + import ( 4 + "bytes" 5 + "encoding/base64" 6 + "encoding/json" 7 + "fmt" 8 + "image" 9 + "io" 10 + "strings" 11 + 12 + "github.com/jphastings/dotpostcard/internal/geom3d" 13 + "github.com/jphastings/dotpostcard/internal/images" 14 + "github.com/jphastings/dotpostcard/types" 15 + ) 16 + 17 + //go:generate qtc -file postcard.svg.qtpl 18 + 19 + type svgVars struct { 20 + size types.Size 21 + b64Img string 22 + frontPoints []geom3d.Point 23 + backPoints []geom3d.Point 24 + metadata []byte 25 + } 26 + 27 + // This keeps the whole JPEG in memory. TODO: Figure out how to do this while streaming 28 + func writeSVG(w io.Writer, pc types.Postcard, combinedImg image.Image, xmpData []byte) error { 29 + frontPoints, err := images.Outline(pc.Front, false, false) 30 + if err != nil { 31 + return fmt.Errorf("front image can't be outlined: %w", err) 32 + } 33 + // TODO: Handle no back 34 + backPoints, err := images.Outline(pc.Back, false, false) 35 + if err != nil { 36 + return fmt.Errorf("back image can't be outlined: %w", err) 37 + } 38 + 39 + var buf bytes.Buffer 40 + b64W := base64.NewEncoder(base64.StdEncoding, &buf) 41 + if err := images.WriteJPEG(b64W, combinedImg, nil); err != nil { 42 + return err 43 + } 44 + 45 + meta, err := json.Marshal(pc.Meta) 46 + if err != nil { 47 + return err 48 + } 49 + 50 + v := svgVars{ 51 + size: pc.Meta.Physical.FrontDimensions, 52 + b64Img: buf.String(), 53 + frontPoints: frontPoints, 54 + backPoints: backPoints, 55 + metadata: meta, 56 + } 57 + 58 + WriteSVG(w, v) 59 + return nil 60 + } 61 + 62 + func pointsToPath(points []geom3d.Point, w, h int, back bool) string { 63 + offsetH := 0 64 + if back { 65 + offsetH = h 66 + } 67 + 68 + var path strings.Builder 69 + for i, p := range points { 70 + if i == 0 { 71 + path.WriteString("M") 72 + } else { 73 + path.WriteString("L") 74 + } 75 + x := p.X * float64(w) 76 + y := p.Y*float64(h) + float64(offsetH) 77 + path.WriteString(fmt.Sprintf("%.1f %.1f", x, y)) 78 + 79 + } 80 + path.WriteString("Z") 81 + return path.String() 82 + }
+10
go.sum
··· 8 8 github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 9 9 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 10 10 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 11 + github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 11 12 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 12 13 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 13 14 github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 14 15 github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 15 16 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 16 17 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 18 + github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 17 19 github.com/calvinfeng/rdp-path-simplification v0.0.0-20180903222510-ae464721f91c h1:rY8nXHWBaXCqDuMfge4bNhbQ/KYgGK0b9ZduB3BosY0= 18 20 github.com/calvinfeng/rdp-path-simplification v0.0.0-20180903222510-ae464721f91c/go.mod h1:5c8wO8qWvpTEFr5p+oeHaCig8KEZAQoLLm24DzsEMwQ= 19 21 github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= ··· 99 101 github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= 100 102 github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4 h1:vCeHcs8N7MOccOOsOVIy1xcYu+kBkA4J5urTgigww7c= 101 103 github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw= 104 + github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 102 105 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 103 106 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 107 + github.com/google/go-units v0.0.0-20250612230646-eddd77f68220/go.mod h1:wBcRMlRM/bVzYk9xtR2hOp3+iWOhEh1FiK8sAzeR9eA= 104 108 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 105 109 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 106 110 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= ··· 115 119 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 116 120 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 117 121 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 122 + github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 118 123 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 119 124 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 120 125 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= ··· 131 136 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 132 137 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 133 138 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 139 + github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 134 140 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 135 141 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 136 142 github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= ··· 157 163 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 158 164 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 159 165 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 166 + github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 160 167 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 161 168 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 162 169 github.com/sunshineplan/tiff v0.0.0-20220128141034-29b9d69bd906 h1:+yYRCj+PGQNnnen4+/Q7eKD2J87RJs+O39bjtHhPauk= 163 170 github.com/sunshineplan/tiff v0.0.0-20220128141034-29b9d69bd906/go.mod h1:O+Ar7ouRbdfxLgoZLFz447/dvdM1NVKk1VpOQaijvAU= 164 171 github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= 165 172 github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 173 + github.com/tmaxmax/go-sse v0.8.0/go.mod h1:HLoxqxdH+7oSUItjtnpxjzJedfr/+Rrm/dNWBcTxJFM= 166 174 github.com/trimmer-io/go-xmp v1.0.0 h1:zY8bolSga5kOjBAaHS6hrdxLgEoYuT875xTy0QDwZWs= 167 175 github.com/trimmer-io/go-xmp v1.0.0/go.mod h1:Aaptr9sp1lLv7UnCAdQ+gSHZyY2miYaKmcNVj7HRBwA= 168 176 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 169 177 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 178 + github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= 170 179 github.com/valyala/quicktemplate v1.8.0 h1:zU0tjbIqTRgKQzFY1L42zq0qR3eh4WoQQdIdqCysW5k= 171 180 github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y8QUsfd1k6L2jM= 172 181 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= ··· 176 185 github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= 177 186 github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= 178 187 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 188 + golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 179 189 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= 180 190 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 181 191 golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
+4 -2
internal/images/outline.go
··· 11 11 "github.com/jphastings/dotpostcard/internal/geom3d" 12 12 ) 13 13 14 + // const linePrecision = 0.0022 15 + const linePrecision = 0.0012 16 + 14 17 // Returns the outline of the image's transparency as an _anticlockwise_ series of X/Y points 15 18 func Outline(im image.Image, invertX, invertY bool) ([]geom3d.Point, error) { 16 19 b := im.Bounds() ··· 49 52 pos, dir = nextPos, nextDir 50 53 } 51 54 52 - ep := 0.0022 53 - path := rdp.SimplifyPath(outline, ep) 55 + path := rdp.SimplifyPath(outline, linePrecision) 54 56 55 57 geomPath := make([]geom3d.Point, len(path)) 56 58 for i, p := range path {