Files
neural-hunt/internal/server/watermark.go
jbergner 005fd6ca51
Some checks failed
release-tag / release-image (push) Failing after 1m18s
RC-1
2026-08-10 05:48:55 +02:00

214 lines
6.5 KiB
Go

package server
import (
"bytes"
"fmt"
"html"
"image"
"image/color"
"image/draw"
_ "image/gif"
_ "image/jpeg"
"image/png"
"net/url"
"os"
"path/filepath"
"strings"
)
// artifactLocalPath maps an artifact URI emitted by the worker back into the
// configured artifact directory. It deliberately rejects traversal and files
// outside /artifacts/ so the public preview endpoint cannot become a generic
// file reader.
func artifactLocalPath(root, artifactURI string) (string, error) {
if strings.TrimSpace(root) == "" {
return "", fmt.Errorf("artifact storage disabled")
}
u, err := url.Parse(artifactURI)
if err != nil {
return "", err
}
p := u.Path
const prefix = "/artifacts/"
i := strings.Index(p, prefix)
if i < 0 {
return "", fmt.Errorf("artifact URI outside artifact namespace")
}
rel := filepath.Clean(filepath.FromSlash(strings.TrimPrefix(p[i:], prefix)))
if rel == "." || rel == "" || filepath.IsAbs(rel) || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("invalid artifact path")
}
rootAbs, err := filepath.Abs(root)
if err != nil {
return "", err
}
candidate := filepath.Join(rootAbs, rel)
candidateAbs, err := filepath.Abs(candidate)
if err != nil {
return "", err
}
check, err := filepath.Rel(rootAbs, candidateAbs)
if err != nil || check == ".." || strings.HasPrefix(check, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("artifact path escapes storage")
}
return candidateAbs, nil
}
func watermarkPreviewFile(path, label string) ([]byte, string, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, "", err
}
if len(b) > 64<<20 {
return nil, "", fmt.Errorf("artifact too large for preview")
}
ext := strings.ToLower(filepath.Ext(path))
if ext == ".svg" || bytes.Contains(bytes.ToLower(b[:minInt(len(b), 512)]), []byte("<svg")) {
out, err := watermarkSVG(b, label)
return out, "image/svg+xml; charset=utf-8", err
}
img, _, err := image.Decode(bytes.NewReader(b))
if err != nil {
return nil, "", fmt.Errorf("decode preview image: %w", err)
}
bounds := img.Bounds()
if bounds.Dx() < 1 || bounds.Dy() < 1 {
return nil, "", fmt.Errorf("empty artifact image")
}
dst := image.NewNRGBA(bounds)
draw.Draw(dst, bounds, img, bounds.Min, draw.Src)
drawRasterWatermark(dst, label)
var out bytes.Buffer
if err := png.Encode(&out, dst); err != nil {
return nil, "", err
}
return out.Bytes(), "image/png", nil
}
func watermarkSVG(src []byte, label string) ([]byte, error) {
s := string(src)
i := strings.LastIndex(strings.ToLower(s), "</svg>")
if i < 0 {
return nil, fmt.Errorf("invalid svg artifact")
}
label = html.EscapeString(strings.ToUpper(strings.TrimSpace(label)))
if label == "" {
label = "NEURAL HUNT PREVIEW"
}
overlay := fmt.Sprintf(`<defs><pattern id="nh-preview-watermark" width="420" height="190" patternUnits="userSpaceOnUse" patternTransform="rotate(-24)"><text x="18" y="96" fill="white" fill-opacity="0.16" font-family="system-ui,sans-serif" font-size="28" font-weight="800" letter-spacing="5">%s</text></pattern></defs><rect x="0" y="0" width="100%%" height="100%%" fill="url(#nh-preview-watermark)" pointer-events="none"/><rect x="2%%" y="92%%" width="96%%" height="5%%" rx="12" fill="black" fill-opacity="0.30"/><text x="50%%" y="95.5%%" text-anchor="middle" fill="white" fill-opacity="0.72" font-family="system-ui,sans-serif" font-size="18" font-weight="800" letter-spacing="4">WATERMARKED LEADERBOARD PREVIEW</text>`, label)
return []byte(s[:i] + overlay + s[i:]), nil
}
var pixelFont = map[rune][7]string{
'A': {"01110", "10001", "10001", "11111", "10001", "10001", "10001"},
'E': {"11111", "10000", "10000", "11110", "10000", "10000", "11111"},
'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"},
'I': {"11111", "00100", "00100", "00100", "00100", "00100", "11111"},
'L': {"10000", "10000", "10000", "10000", "10000", "10000", "11111"},
'N': {"10001", "11001", "10101", "10101", "10011", "10001", "10001"},
'P': {"11110", "10001", "10001", "11110", "10000", "10000", "10000"},
'R': {"11110", "10001", "10001", "11110", "10100", "10010", "10001"},
'T': {"11111", "00100", "00100", "00100", "00100", "00100", "00100"},
'U': {"10001", "10001", "10001", "10001", "10001", "10001", "01110"},
'V': {"10001", "10001", "10001", "10001", "10001", "01010", "00100"},
'W': {"10001", "10001", "10001", "10101", "10101", "10101", "01010"},
}
func drawRasterWatermark(img *image.NRGBA, label string) {
b := img.Bounds()
w, h := b.Dx(), b.Dy()
if w < 1 || h < 1 {
return
}
label = strings.ToUpper(strings.TrimSpace(label))
if label == "" {
label = "NEURAL HUNT PREVIEW"
}
// Keep the bitmap watermark large enough to survive thumbnail scaling.
scale := minInt(w, h) / 360
if scale < 1 {
scale = 1
}
if scale > 6 {
scale = 6
}
textW := pixelTextWidth(label, scale)
rowStep := 72 * scale
colStep := textW + 54*scale
for y, row := b.Min.Y+18*scale, 0; y < b.Max.Y; y, row = y+rowStep, row+1 {
offset := 0
if row%2 == 1 {
offset = -(colStep / 2)
}
for x := b.Min.X + offset; x < b.Max.X; x += colStep {
drawPixelText(img, x+scale, y+scale, label, scale, color.NRGBA{0, 0, 0, 72})
drawPixelText(img, x, y, label, scale, color.NRGBA{255, 255, 255, 48})
}
}
// Strong lower preview band so cropped screenshots still visibly carry a
// watermark. It contains a repeated NH glyph rather than metadata.
bandH := maxInt(16*scale, h/18)
bandY := b.Max.Y - bandH
draw.Draw(img, image.Rect(b.Min.X, bandY, b.Max.X, b.Max.Y), &image.Uniform{C: color.NRGBA{0, 0, 0, 92}}, image.Point{}, draw.Over)
for x := b.Min.X + 8*scale; x < b.Max.X; x += 40 * scale {
drawPixelText(img, x, bandY+4*scale, "NH", scale, color.NRGBA{255, 255, 255, 118})
}
}
func pixelTextWidth(text string, scale int) int {
if scale < 1 {
scale = 1
}
w := 0
for _, r := range text {
if r == ' ' {
w += 4 * scale
} else {
w += 6 * scale
}
}
return w
}
func drawPixelText(dst draw.Image, x, y int, text string, scale int, col color.Color) {
if scale < 1 {
scale = 1
}
cx := x
for _, r := range text {
if r == ' ' {
cx += 4 * scale
continue
}
glyph, ok := pixelFont[r]
if !ok {
cx += 6 * scale
continue
}
for gy, row := range glyph {
for gx, bit := range row {
if bit != '1' {
continue
}
rect := image.Rect(cx+gx*scale, y+gy*scale, cx+(gx+1)*scale, y+(gy+1)*scale)
draw.Draw(dst, rect, &image.Uniform{C: col}, image.Point{}, draw.Over)
}
}
cx += 6 * scale
}
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}