Implement Phase 5: SVG, JPG, PDF rendering
- SVG: strokes → SVG path elements with dashed/arrow support, coordinates scaled from 300 DPI to 72 DPI - JPG: rasterization at 300 DPI using Go image package, Bresenham line drawing with round pen circles - PDF: minimal PDF generation with raw operators, no external library - 6 tests: SVG output, dashed style, arrow heads, JPG magic bytes, PDF header, page size calculations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
98
internal/render/jpg.go
Normal file
98
internal/render/jpg.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"math"
|
||||
)
|
||||
|
||||
// RenderJPG rasterizes a page's strokes at 300 DPI as JPEG.
|
||||
func RenderJPG(pageSize string, strokes []Stroke, quality int) ([]byte, error) {
|
||||
w, h := pageSizeCanonical(pageSize)
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
|
||||
// White background
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
img.Set(x, y, color.White)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw strokes
|
||||
for _, s := range strokes {
|
||||
points := decodePoints(s.PointData)
|
||||
if len(points) < 4 {
|
||||
continue
|
||||
}
|
||||
c := argbToColor(s.Color)
|
||||
penRadius := float64(s.PenSize) / 2.0
|
||||
|
||||
for i := 0; i < len(points)-3; i += 2 {
|
||||
x0, y0 := points[i], points[i+1]
|
||||
x1, y1 := points[i+2], points[i+3]
|
||||
drawLine(img, x0, y0, x1, y1, penRadius, c)
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func pageSizeCanonical(pageSize string) (int, int) {
|
||||
switch pageSize {
|
||||
case "LARGE":
|
||||
return 3300, 5100
|
||||
default:
|
||||
return 2550, 3300
|
||||
}
|
||||
}
|
||||
|
||||
func argbToColor(argb int32) color.RGBA {
|
||||
return color.RGBA{
|
||||
R: uint8((argb >> 16) & 0xFF),
|
||||
G: uint8((argb >> 8) & 0xFF),
|
||||
B: uint8(argb & 0xFF),
|
||||
A: uint8((argb >> 24) & 0xFF),
|
||||
}
|
||||
}
|
||||
|
||||
// drawLine draws a line with the given pen radius using Bresenham's.
|
||||
func drawLine(img *image.RGBA, x0, y0, x1, y1, radius float64, c color.RGBA) {
|
||||
dx := x1 - x0
|
||||
dy := y1 - y0
|
||||
dist := math.Sqrt(dx*dx + dy*dy)
|
||||
if dist < 1 {
|
||||
fillCircle(img, int(x0), int(y0), int(radius), c)
|
||||
return
|
||||
}
|
||||
|
||||
steps := int(dist) + 1
|
||||
for i := 0; i <= steps; i++ {
|
||||
t := float64(i) / float64(steps)
|
||||
x := x0 + t*dx
|
||||
y := y0 + t*dy
|
||||
fillCircle(img, int(x), int(y), int(radius), c)
|
||||
}
|
||||
}
|
||||
|
||||
func fillCircle(img *image.RGBA, cx, cy, r int, c color.RGBA) {
|
||||
if r < 1 {
|
||||
r = 1
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
for dy := -r; dy <= r; dy++ {
|
||||
for dx := -r; dx <= r; dx++ {
|
||||
if dx*dx+dy*dy <= r*r {
|
||||
px, py := cx+dx, cy+dy
|
||||
if px >= bounds.Min.X && px < bounds.Max.X && py >= bounds.Min.Y && py < bounds.Max.Y {
|
||||
img.Set(px, py, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user