Skip to content

Commit b821567

Browse files
committed
feat(img): create a docs image backend to render images from embedded fs instead of users file system
1 parent bca6b5b commit b821567

File tree

2 files changed

+163
-0
lines changed

2 files changed

+163
-0
lines changed

internal/img/docs_backend.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package img
2+
3+
import (
4+
"fmt"
5+
"image"
6+
"image/draw"
7+
"image/jpeg"
8+
"image/png"
9+
"os"
10+
"path/filepath"
11+
12+
"github.com/ploMP4/chafa-go"
13+
14+
"github.com/museslabs/kyma/docs"
15+
)
16+
17+
// docsBackend is an image backend tailored for documentation rendering.
18+
// It loads images from an embedded filesystem instead of the user's local FS.
19+
type docsBackend struct {
20+
cache *chafaCache
21+
}
22+
23+
// NewDocsBackend creates a new instance of docsBackend with an initialized cache.
24+
func NewDocsBackend() *docsBackend {
25+
return &docsBackend{
26+
cache: &chafaCache{
27+
cache: map[string]string{},
28+
},
29+
}
30+
}
31+
32+
func (b *docsBackend) SymbolsOnly() bool {
33+
return b.detectTerminal().pixelMode == chafa.CHAFA_PIXEL_MODE_SYMBOLS
34+
}
35+
36+
func (b *docsBackend) Render(path string, width, height int, symbols bool) (string, error) {
37+
var err error
38+
39+
defer func() {
40+
if r := recover(); r != nil {
41+
if recErr, ok := r.(error); ok {
42+
err = recErr
43+
} else {
44+
err = fmt.Errorf("panic: %v", r)
45+
}
46+
}
47+
}()
48+
49+
c := b.cache.Get(path, width, height, symbols)
50+
if c != "" {
51+
return c, nil
52+
}
53+
54+
out, err := b.render(path, int32(width), int32(height), symbols)
55+
b.cache.Save(out, path, width, height, symbols)
56+
57+
return out, err
58+
}
59+
60+
func (b docsBackend) render(path string, width, height int32, symbols bool) (string, error) {
61+
pixels, pixelWidth, pixelHeight, err := b.load(path)
62+
if err != nil {
63+
return "", err
64+
}
65+
66+
chafa.CalcCanvasGeometry(width, height, &width, &height, 1, true, false)
67+
68+
capabilities := b.detectTerminal()
69+
defer chafa.SymbolMapUnref(capabilities.symbolMap)
70+
defer chafa.TermInfoUnref(capabilities.termInfo)
71+
72+
config := chafa.CanvasConfigNew()
73+
defer chafa.CanvasConfigUnref(config)
74+
75+
chafa.CanvasConfigSetCanvasMode(config, capabilities.canvasMode)
76+
chafa.CanvasConfigSetGeometry(config, width, height)
77+
chafa.CanvasConfigSetPassthrough(config, capabilities.passthrough)
78+
chafa.CanvasConfigSetSymbolMap(config, capabilities.symbolMap)
79+
chafa.CanvasConfigSetCellGeometry(config, 18, 36)
80+
81+
if symbols {
82+
chafa.CanvasConfigSetPixelMode(config, chafa.CHAFA_PIXEL_MODE_SYMBOLS)
83+
} else {
84+
chafa.CanvasConfigSetPixelMode(config, capabilities.pixelMode)
85+
}
86+
87+
canvas := chafa.CanvasNew(config)
88+
defer chafa.CanvasUnRef(canvas)
89+
90+
chafa.CanvasDrawAllPixels(
91+
canvas,
92+
chafa.CHAFA_PIXEL_RGBA8_UNASSOCIATED,
93+
pixels,
94+
pixelWidth,
95+
pixelHeight,
96+
pixelWidth*nChannels,
97+
)
98+
printable := chafa.CanvasPrint(canvas, nil)
99+
100+
return printable.String(), nil
101+
}
102+
103+
func (b docsBackend) detectTerminal() chafaTerminalCapabilities {
104+
termInfo := chafa.TermDbDetect(chafa.TermDbGetDefault(), os.Environ())
105+
106+
mode := chafa.TermInfoGetBestCanvasMode(termInfo)
107+
pixelMode := chafa.TermInfoGetBestPixelMode(termInfo)
108+
109+
passthrough := chafa.CHAFA_PASSTHROUGH_NONE
110+
if chafa.TermInfoGetIsPixelPassthroughNeeded(termInfo, pixelMode) {
111+
passthrough = chafa.TermInfoGetPassthroughType(termInfo)
112+
}
113+
114+
symbolMap := chafa.SymbolMapNew()
115+
chafa.SymbolMapAddByTags(symbolMap, chafa.TermInfoGetSafeSymbolTags(termInfo))
116+
117+
return chafaTerminalCapabilities{
118+
termInfo: termInfo,
119+
canvasMode: mode,
120+
pixelMode: pixelMode,
121+
passthrough: passthrough,
122+
symbolMap: symbolMap,
123+
}
124+
}
125+
126+
func (b docsBackend) load(path string) (pixels []uint8, width, height int32, err error) {
127+
file, err := docs.FS.Open(path)
128+
if err != nil {
129+
return nil, 0, 0, err
130+
}
131+
defer file.Close()
132+
133+
var img image.Image
134+
135+
switch filepath.Ext(path) {
136+
case "png":
137+
img, err = png.Decode(file)
138+
if err != nil {
139+
return nil, 0, 0, err
140+
}
141+
case "jpg", "jpeg":
142+
img, err = jpeg.Decode(file)
143+
if err != nil {
144+
return nil, 0, 0, err
145+
}
146+
default:
147+
img, _, err = image.Decode(file)
148+
if err != nil {
149+
return nil, 0, 0, err
150+
}
151+
}
152+
153+
bounds := img.Bounds()
154+
width = int32(bounds.Dx())
155+
height = int32(bounds.Dy())
156+
157+
rgbaImg := image.NewRGBA(bounds)
158+
draw.Draw(rgbaImg, bounds, img, bounds.Min, draw.Src)
159+
160+
return rgbaImg.Pix, width, height, nil
161+
}

internal/img/images.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ type ImageBackend interface {
77

88
func Get(backend string) ImageBackend {
99
switch backend {
10+
case "docs":
11+
return NewDocsBackend()
1012
case "chafa":
1113
fallthrough
1214
default:

0 commit comments

Comments
 (0)