Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor font handling and embed assets instead of relative path #4

Merged
merged 2 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion stl/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func generateText(username string, startYear int, endYear int, dims modelDimensi
// generateLogo handles the generation of the GitHub logo geometry
func generateLogo(dims modelDimensions, ch chan<- geometryResult, wg *sync.WaitGroup) {
defer wg.Done()
logoTriangles, err := geometry.GenerateImageGeometry(dims.imagePath, dims.innerWidth, geometry.BaseHeight)
logoTriangles, err := geometry.GenerateImageGeometry(dims.innerWidth, geometry.BaseHeight)
if err != nil {
// Log warning and continue without logo instead of failing
if logErr := logger.GetLogger().Warning("Failed to generate logo geometry: %v. Continuing without logo.", err); logErr != nil {
Expand Down
73 changes: 73 additions & 0 deletions stl/geometry/assets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package geometry

import (
"embed"
"fmt"
"os"

"github.com/github/gh-skyline/errors"
)

//go:embed assets/*
var embeddedAssets embed.FS

// writeTempFont writes the embedded font to a temporary file and returns its path.
// The caller is responsible for cleaning up the temporary file.
func writeTempFont(fontName string) (string, func(), error) {
fontBytes, err := embeddedAssets.ReadFile("assets/" + fontName)
if err != nil {
return "", nil, errors.New(errors.IOError, "failed to read embedded font", err)
}

// Create temp file with .ttf extension to ensure proper font loading
tmpFile, err := os.CreateTemp("", "skyline-font-*.ttf")
if err != nil {
return "", nil, errors.New(errors.IOError, "failed to create temp font file", err)
}

if _, err := tmpFile.Write(fontBytes); err != nil {
closeErr := tmpFile.Close()
removeErr := os.Remove(tmpFile.Name())
return "", nil, errors.New(errors.IOError, "failed to write font to temp file", fmt.Errorf("%w; close error: %v; remove error: %v", err, closeErr, removeErr))
}
if err := tmpFile.Close(); err != nil {
removeErr := os.Remove(tmpFile.Name())
return "", nil, errors.New(errors.IOError, "failed to close temp font file", fmt.Errorf("%w; remove error: %v", err, removeErr))
}

cleanup := func() {
_ = os.Remove(tmpFile.Name()) // Ignore cleanup errors in defer
}

return tmpFile.Name(), cleanup, nil
}

// getEmbeddedImage returns a temporary file path for the embedded image.
// The caller is responsible for cleaning up the temporary file.
func getEmbeddedImage() (string, func(), error) {
imgBytes, err := embeddedAssets.ReadFile("assets/invertocat.png")
if err != nil {
return "", nil, errors.New(errors.IOError, "failed to read embedded image", err)
}

tmpFile, err := os.CreateTemp("", "skyline-img-*.png")
if err != nil {
return "", nil, errors.New(errors.IOError, "failed to create temp image file", err)
}

if _, err := tmpFile.Write(imgBytes); err != nil {
closeErr := tmpFile.Close()
removeErr := os.Remove(tmpFile.Name())
return "", nil, errors.New(errors.IOError, "failed to write image to temp file", fmt.Errorf("%w; close error: %v; remove error: %v", err, closeErr, removeErr))
}
if err := tmpFile.Close(); err != nil {
removeErr := os.Remove(tmpFile.Name())
return "", nil, errors.New(errors.IOError, "failed to close temp image file", fmt.Errorf("%w; remove error: %v", err, removeErr))
}

cleanup := func() {
_ = os.Remove(tmpFile.Name()) // Ignore cleanup errors in defer
}

return tmpFile.Name(), cleanup, nil
}
File renamed without changes
File renamed without changes.
86 changes: 86 additions & 0 deletions stl/geometry/assets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package geometry

import (
"os"
"path/filepath"
"testing"
)

// TestWriteTempFont verifies temporary font file creation and cleanup
func TestWriteTempFont(t *testing.T) {
t.Run("verify valid font extraction", func(t *testing.T) {
fontPath, cleanup, err := writeTempFont("monasans-medium.ttf")
if err != nil {
t.Fatalf("writeTempFont failed: %v", err)
}
defer cleanup()

// Verify file exists and has content
content, err := os.ReadFile(fontPath)
if err != nil {
t.Errorf("Failed to read temp font file: %v", err)
}
if len(content) == 0 {
t.Error("Temp font file is empty")
}

// Verify file extension
if filepath.Ext(fontPath) != ".ttf" {
t.Errorf("Expected .ttf extension, got %s", filepath.Ext(fontPath))
}

// Verify cleanup works
cleanup()
if _, err := os.Stat(fontPath); !os.IsNotExist(err) {
t.Error("Temp font file not cleaned up properly")
}
})

t.Run("verify nonexistent font handling", func(t *testing.T) {
_, cleanup, err := writeTempFont("nonexistent.ttf")
if err == nil {
defer cleanup()
t.Error("Expected error for nonexistent font")
}
})
}

// TestGetEmbeddedImage verifies temporary image file creation and cleanup
func TestGetEmbeddedImage(t *testing.T) {
t.Run("verify valid image extraction", func(t *testing.T) {
imagePath, cleanup, err := getEmbeddedImage()
if err != nil {
t.Fatalf("getEmbeddedImage failed: %v", err)
}
defer cleanup()

// Verify file exists and has content
content, err := os.ReadFile(imagePath)
if err != nil {
t.Errorf("Failed to read temp image file: %v", err)
}
if len(content) == 0 {
t.Error("Temp image file is empty")
}

// Verify file extension
if filepath.Ext(imagePath) != ".png" {
t.Errorf("Expected .png extension, got %s", filepath.Ext(imagePath))
}

// Verify cleanup works
cleanup()
if _, err := os.Stat(imagePath); !os.IsNotExist(err) {
t.Error("Temp image file not cleaned up properly")
}
})

// Test embedded filesystem access
t.Run("verify embedded filesystem access", func(t *testing.T) {
// Try to read the embedded image directly
_, err := embeddedAssets.ReadFile("assets/invertocat.png")
if err != nil {
t.Errorf("Failed to access embedded image: %v", err)
}
})
}
4 changes: 2 additions & 2 deletions stl/geometry/geometry.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const (

// Font file paths for text rendering.
const (
PrimaryFont = "assets/monasans-medium.ttf"
FallbackFont = "assets/monasans-regular.ttf"
PrimaryFont = "monasans-medium.ttf"
FallbackFont = "monasans-regular.ttf"
)

// Additional constants for year range styling
Expand Down
31 changes: 25 additions & 6 deletions stl/geometry/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,21 @@ func Create3DText(username string, year string, innerWidth, baseHeight float64)
// renderText generates 3D geometry for the given text configuration.
func renderText(config textRenderConfig) ([]types.Triangle, error) {
dc := gg.NewContext(config.contextWidth, config.contextHeight)
if err := dc.LoadFontFace(PrimaryFont, config.fontSize); err != nil {
if err := dc.LoadFontFace(FallbackFont, config.fontSize); err != nil {
return nil, errors.New(errors.IOError, "failed to load fonts", err)

// Get temporary font file
fontPath, cleanup, err := writeTempFont(PrimaryFont)
if err != nil {
// Try fallback font
fontPath, cleanup, err = writeTempFont(FallbackFont)
if err != nil {
return nil, errors.New(errors.IOError, "failed to load any fonts", err)
}
}

if err := dc.LoadFontFace(fontPath, config.fontSize); err != nil {
return nil, errors.New(errors.IOError, "failed to load font", err)
}

dc.SetRGB(0, 0, 0)
dc.Clear()
dc.SetRGB(1, 1, 1)
Expand Down Expand Up @@ -147,11 +156,19 @@ func renderText(config textRenderConfig) ([]types.Triangle, error) {
}
}

defer cleanup()

return triangles, nil
}

// GenerateImageGeometry creates 3D geometry from a PNG image file.
func GenerateImageGeometry(imagePath string, innerWidth, baseHeight float64) ([]types.Triangle, error) {
// GenerateImageGeometry creates 3D geometry from the embedded logo image.
func GenerateImageGeometry(innerWidth, baseHeight float64) ([]types.Triangle, error) {
// Get temporary image file
imgPath, cleanup, err := getEmbeddedImage()
if err != nil {
return nil, err
}

config := imageRenderConfig{
renderConfig: renderConfig{
startX: innerWidth * imagePosition,
Expand All @@ -160,10 +177,12 @@ func GenerateImageGeometry(imagePath string, innerWidth, baseHeight float64) ([]
voxelScale: defaultImageScale,
depth: frontEmbedDepth,
},
imagePath: imagePath,
imagePath: imgPath,
height: defaultImageHeight,
}

defer cleanup()

return renderImage(config)
}

Expand Down
11 changes: 2 additions & 9 deletions stl/geometry/text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestGenerateImageGeometry(t *testing.T) {
}()

t.Run("verify valid image geometry generation", func(t *testing.T) {
triangles, err := GenerateImageGeometry(testPNGPath, 100.0, 5.0)
triangles, err := GenerateImageGeometry(100.0, 5.0)
if err != nil {
t.Fatalf("GenerateImageGeometry failed: %v", err)
}
Expand All @@ -176,15 +176,8 @@ func TestGenerateImageGeometry(t *testing.T) {
}
})

t.Run("verify invalid image path", func(t *testing.T) {
_, err := GenerateImageGeometry("nonexistent.png", 100.0, 5.0)
if err == nil {
t.Error("Expected error for invalid image path")
}
})

t.Run("verify geometry normal vectors", func(t *testing.T) {
triangles, err := GenerateImageGeometry(testPNGPath, 100.0, 5.0)
triangles, err := GenerateImageGeometry(100.0, 5.0)
if err != nil {
t.Fatalf("GenerateImageGeometry failed: %v", err)
}
Expand Down
Loading