diff --git a/backend/internal/sys/config/conf.go b/backend/internal/sys/config/conf.go
index bad0ccf0..3efbacd1 100644
--- a/backend/internal/sys/config/conf.go
+++ b/backend/internal/sys/config/conf.go
@@ -71,6 +71,8 @@ type LabelMakerConf struct {
DynamicLength bool `yaml:"bool" conf:"default:true"`
LabelServiceUrl *string `yaml:"label_service_url"`
LabelServiceTimeout *time.Duration `yaml:"label_service_timeout"`
+ RegularFontPath *string `yaml:"regular_font_path"`
+ BoldFontPath *string `yaml:"bold_font_path"`
}
type BarcodeAPIConf struct {
diff --git a/backend/pkgs/labelmaker/labelmaker.go b/backend/pkgs/labelmaker/labelmaker.go
index a28bb72a..49dc3f0d 100644
--- a/backend/pkgs/labelmaker/labelmaker.go
+++ b/backend/pkgs/labelmaker/labelmaker.go
@@ -27,6 +27,13 @@ import (
"golang.org/x/image/font/gofont/gomedium"
)
+type FontType int
+
+const (
+ FontTypeRegular FontType = iota
+ FontTypeBold
+)
+
type GenerateParameters struct {
Width int
Height int
@@ -140,6 +147,48 @@ func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeig
return wrappedLines, ""
}
+func loadFont(cfg *config.Config, fontType FontType) (*truetype.Font, error) {
+ var fontPath *string
+ var fallbackData []byte
+
+ switch fontType {
+ case FontTypeRegular:
+ if cfg != nil && cfg.LabelMaker.RegularFontPath != nil {
+ fontPath = cfg.LabelMaker.RegularFontPath
+ }
+ fallbackData = gomedium.TTF
+ case FontTypeBold:
+ if cfg != nil && cfg.LabelMaker.BoldFontPath != nil {
+ fontPath = cfg.LabelMaker.BoldFontPath
+ }
+ fallbackData = gobold.TTF
+ default:
+ return nil, fmt.Errorf("unknown font type: %d", fontType)
+ }
+
+ if fontPath != nil && *fontPath != "" {
+ data, err := os.ReadFile(*fontPath)
+ if err != nil {
+ log.Printf("Failed to load font from %s: %v, using fallback font", *fontPath, err)
+ } else {
+ font, err := truetype.Parse(data)
+ if err != nil {
+ log.Printf("Failed to parse font from %s: %v, using fallback font", *fontPath, err)
+ } else {
+ log.Printf("Successfully loaded font from %s", *fontPath)
+ return font, nil
+ }
+ }
+ }
+
+ font, err := truetype.Parse(fallbackData)
+ if err != nil {
+ return nil, err
+ }
+
+ return font, nil
+}
+
func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config) error {
if err := params.Validate(); err != nil {
return err
@@ -165,12 +214,12 @@ func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config)
qr.DisableBorder = true
qrImage := qr.Image(params.QrSize)
- regularFont, err := truetype.Parse(gomedium.TTF)
+ regularFont, err := loadFont(cfg, FontTypeRegular)
if err != nil {
return err
}
- boldFont, err := truetype.Parse(gobold.TTF)
+ boldFont, err := loadFont(cfg, FontTypeBold)
if err != nil {
return err
}
diff --git a/backend/pkgs/labelmaker/labelmaker_test.go b/backend/pkgs/labelmaker/labelmaker_test.go
new file mode 100644
index 00000000..3ee3d73c
--- /dev/null
+++ b/backend/pkgs/labelmaker/labelmaker_test.go
@@ -0,0 +1,187 @@
+package labelmaker
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/sysadminsmedia/homebox/backend/internal/sys/config"
+ "golang.org/x/image/font/gofont/gobold"
+ "golang.org/x/image/font/gofont/gomedium"
+)
+
+func TestLoadFont_WithNilConfig(t *testing.T) {
+ font, err := loadFont(nil, FontTypeRegular)
+ require.NoError(t, err)
+ assert.NotNil(t, font)
+
+ font, err = loadFont(nil, FontTypeBold)
+ require.NoError(t, err)
+ assert.NotNil(t, font)
+}
+
+func TestLoadFont_WithEmptyConfig(t *testing.T) {
+ cfg := &config.Config{}
+
+ font, err := loadFont(cfg, FontTypeRegular)
+ require.NoError(t, err)
+ assert.NotNil(t, font)
+
+ font, err = loadFont(cfg, FontTypeBold)
+ require.NoError(t, err)
+ assert.NotNil(t, font)
+}
+
+func TestLoadFont_WithCustomFontPath(t *testing.T) {
+ tempDir := t.TempDir()
+ fontPath := filepath.Join(tempDir, "test-font.ttf")
+
+ err := os.WriteFile(fontPath, gomedium.TTF, 0644)
+ require.NoError(t, err)
+
+ cfg := &config.Config{
+ LabelMaker: config.LabelMakerConf{
+ RegularFontPath: &fontPath,
+ },
+ }
+
+ font, err := loadFont(cfg, FontTypeRegular)
+ require.NoError(t, err)
+ assert.NotNil(t, font)
+}
+
+func TestLoadFont_WithNonExistentFontPath(t *testing.T) {
+ nonExistentPath := "/non/existent/path/font.ttf"
+ cfg := &config.Config{
+ LabelMaker: config.LabelMakerConf{
+ RegularFontPath: &nonExistentPath,
+ },
+ }
+
+ font, err := loadFont(cfg, FontTypeRegular)
+ require.NoError(t, err)
+ assert.NotNil(t, font)
+}
+
+func TestLoadFont_UnknownFontType(t *testing.T) {
+ cfg := &config.Config{}
+
+ _, err := loadFont(cfg, FontType(999))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "unknown font type")
+}
+
+func TestLoadFont_BoldFontWithCustomPath(t *testing.T) {
+ tempDir := t.TempDir()
+ fontPath := filepath.Join(tempDir, "test-bold-font.ttf")
+
+ err := os.WriteFile(fontPath, gobold.TTF, 0644)
+ require.NoError(t, err)
+
+ cfg := &config.Config{
+ LabelMaker: config.LabelMakerConf{
+ BoldFontPath: &fontPath,
+ },
+ }
+
+ font, err := loadFont(cfg, FontTypeBold)
+ require.NoError(t, err)
+ assert.NotNil(t, font)
+}
+
+func TestLoadFont_EmptyStringPath(t *testing.T) {
+ emptyPath := ""
+ cfg := &config.Config{
+ LabelMaker: config.LabelMakerConf{
+ RegularFontPath: &emptyPath,
+ },
+ }
+
+ font, err := loadFont(cfg, FontTypeRegular)
+ require.NoError(t, err)
+ assert.NotNil(t, font)
+}
+
+func TestLoadFont_CJKRendering(t *testing.T) {
+ cjkFontPath := filepath.Join("testdata", "NotoSansKR-VF.ttf")
+
+ tests := []struct {
+ name string
+ text string
+ fontPath string
+ shouldHaveGlyph bool
+ }{
+ {
+ name: "Korean with default font",
+ text: "한글",
+ fontPath: "",
+ shouldHaveGlyph: false,
+ },
+ {
+ name: "Chinese with default font",
+ text: "中文",
+ fontPath: "",
+ shouldHaveGlyph: false,
+ },
+ {
+ name: "Japanese with default font",
+ text: "ひらがなカタカナ",
+ fontPath: "",
+ shouldHaveGlyph: false,
+ },
+ {
+ name: "Korean with Noto Sans CJK",
+ text: "한글",
+ fontPath: cjkFontPath,
+ shouldHaveGlyph: true,
+ },
+ {
+ name: "Chinese with Noto Sans CJK",
+ text: "中文",
+ fontPath: cjkFontPath,
+ shouldHaveGlyph: true,
+ },
+ {
+ name: "Japanese with Noto Sans CJK",
+ text: "ひらがなカタカナ",
+ fontPath: cjkFontPath,
+ shouldHaveGlyph: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var cfg *config.Config
+ if tt.fontPath != "" {
+ if _, err := os.Stat(tt.fontPath); os.IsNotExist(err) {
+ t.Skipf("Font file not found: %s", tt.fontPath)
+ }
+ cfg = &config.Config{
+ LabelMaker: config.LabelMakerConf{
+ RegularFontPath: &tt.fontPath,
+ },
+ }
+ }
+
+ font, err := loadFont(cfg, FontTypeRegular)
+ require.NoError(t, err)
+ require.NotNil(t, font)
+
+ hasAllGlyphs := true
+ for _, r := range tt.text {
+ if font.Index(r) == 0 {
+ hasAllGlyphs = false
+ break
+ }
+ }
+
+ if tt.shouldHaveGlyph {
+ assert.True(t, hasAllGlyphs, "Font should render %s characters", tt.name)
+ } else {
+ assert.False(t, hasAllGlyphs, "Default font should not render %s characters", tt.name)
+ }
+ })
+ }
+}
diff --git a/backend/pkgs/labelmaker/testdata/NotoSans-LICENSE b/backend/pkgs/labelmaker/testdata/NotoSans-LICENSE
new file mode 100644
index 00000000..d952d62c
--- /dev/null
+++ b/backend/pkgs/labelmaker/testdata/NotoSans-LICENSE
@@ -0,0 +1,92 @@
+This Font Software is licensed under the SIL Open Font License,
+Version 1.1.
+
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font
+creation efforts of academic and linguistic communities, and to
+provide a free and open framework in which fonts may be shared and
+improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply to
+any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software
+components as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to,
+deleting, or substituting -- in part or in whole -- any of the
+components of the Original Version, by changing formats or by porting
+the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed,
+modify, redistribute, and sell modified and unmodified copies of the
+Font Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components, in
+Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the
+corresponding Copyright Holder. This restriction only applies to the
+primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created using
+the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/backend/pkgs/labelmaker/testdata/NotoSansKR-VF.ttf b/backend/pkgs/labelmaker/testdata/NotoSansKR-VF.ttf
new file mode 100644
index 00000000..15b3f19b
Binary files /dev/null and b/backend/pkgs/labelmaker/testdata/NotoSansKR-VF.ttf differ
diff --git a/backend/pkgs/labelmaker/testdata/README.md b/backend/pkgs/labelmaker/testdata/README.md
new file mode 100644
index 00000000..b37ff888
--- /dev/null
+++ b/backend/pkgs/labelmaker/testdata/README.md
@@ -0,0 +1,15 @@
+# Test Data
+
+This directory contains font files used only for testing purposes.
+
+## Fonts
+
+- **NotoSansKR-VF.ttf**: Noto Sans CJK Korean Variable Font
+ - Used for testing CJK (Chinese, Japanese, Korean) character rendering
+ - License: See `NotoSans-LICENSE` file
+
+## Notes
+
+- These fonts are **only used during testing** and are **not included in production builds**
+- Go's build system automatically excludes `testdata` directories from production builds
+- The fonts support rendering of Korean, Chinese, and Japanese characters
diff --git a/docs/en/configure/index.md b/docs/en/configure/index.md
index d3fc2cde..61aa8f18 100644
--- a/docs/en/configure/index.md
+++ b/docs/en/configure/index.md
@@ -48,6 +48,8 @@ aside: false
| HBOX_LABEL_MAKER_PRINT_COMMAND | | the command to use for printing labels. if empty, label printing is disabled. `{{.FileName}}` in the command will be replaced with the png filename of the label |
| HBOX_LABEL_MAKER_DYNAMIC_LENGTH | true | allow label generation with open length. `HBOX_LABEL_MAKER_HEIGHT` is still used for layout and minimal height. If not used, long text may be cut off, but all labels have the same size. |
| HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION | | Additional information added to the label like name or phone number |
+| HBOX_LABEL_MAKER_REGULAR_FONT_PATH | | path to regular font file for label generation (e.g., `/fonts/NotoSansKR-Regular.ttf`). If not set, uses embedded font. Supports TTF format. |
+| HBOX_LABEL_MAKER_BOLD_FONT_PATH | | path to bold font file for label generation (e.g., `/fonts/NotoSansKR-Bold.ttf`). If not set, uses embedded font. Supports TTF format. |
| HBOX_THUMBNAIL_ENABLED | true | enable thumbnail generation for images, supports PNG, JPEG, AVIF, WEBP, GIF file types |
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
@@ -192,6 +194,8 @@ OPTIONS
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND
--label-maker-dynamic-length/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH (default: true)
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION
+--label-maker-regular-font-path/$HBOX_LABEL_MAKER_REGULAR_FONT_PATH
+--label-maker-bold-font-path/$HBOX_LABEL_MAKER_BOLD_FONT_PATH
--thumbnail-enabled/$HBOX_THUMBNAIL_ENABLED (default: true)
--thumbnail-width/$HBOX_THUMBNAIL_WIDTH (default: 500)
--thumbnail-height/$HBOX_THUMBNAIL_HEIGHT (default: 500)
diff --git a/docs/en/custom-font-setup.md b/docs/en/custom-font-setup.md
new file mode 100644
index 00000000..69f0023c
--- /dev/null
+++ b/docs/en/custom-font-setup.md
@@ -0,0 +1,80 @@
+# External Font Support for Label Maker
+
+Label maker supports external font files.
+
+## Quick Start
+
+### Docker/Podman Setup
+
+1. **Download external fonts** (e.g., Noto Sans KR):
+ - Download from [Google Fonts](https://fonts.google.com/noto/specimen/Noto+Sans+KR)
+ - Or use the Variable Font from [GitHub](https://github.com/notofonts/noto-cjk)
+
+2. **Create a fonts directory**:
+```bash
+mkdir -p ./fonts
+# Place your font files in this directory
+# e.g., NotoSansKR-VF.ttf
+```
+
+3. **Mount the fonts directory and set environment variables**:
+```yaml
+# docker-compose.yml
+services:
+ homebox:
+ image: homebox:latest
+ volumes:
+ - ./data:/data
+ - ./fonts:/fonts:ro # Mount fonts directory as read-only
+ environment:
+ - HBOX_LABEL_MAKER_REGULAR_FONT_PATH=/fonts/NotoSansKR-VF.ttf
+ - HBOX_LABEL_MAKER_BOLD_FONT_PATH=/fonts/NotoSansKR-VF.ttf
+ ports:
+ - 3100:7745
+```
+
+Or with podman:
+```bash
+podman run -d \
+ --name homebox \
+ -p 3100:7745 \
+ -v ./data:/data \
+ -v ./fonts:/fonts:ro \
+ -e HBOX_LABEL_MAKER_REGULAR_FONT_PATH=/fonts/NotoSansKR-VF.ttf \
+ -e HBOX_LABEL_MAKER_BOLD_FONT_PATH=/fonts/NotoSansKR-VF.ttf \
+ homebox:latest
+```
+
+4. **Restart the container** and test label generation with Chinese, Japanese, Korean text!
+
+## Supported Fonts
+
+- **Format**: TTF (TrueType Font)
+- **Recommended Fonts**:
+ - Noto Sans KR (Korean)
+ - Noto Sans CJK (Chinese, Japanese, Korean)
+ - Noto Sans SC (Simplified Chinese)
+ - Noto Sans JP (Japanese)
+
+## Fallback Behavior
+
+1. **External font specified** → Tries to load from `HBOX_LABEL_MAKER_*_FONT_PATH`
+2. **External font fails or not specified** → Falls back to bundled Go fonts (Latin-only, **does not support CJK characters**)
+
+## Troubleshooting
+
+### Labels still show squares (□□□)
+- Check if the font file exists at the specified path
+- Verify the font file format (must be TTF, not OTF)
+- Check container logs: `podman logs homebox | grep -i font`
+
+### Font file not found
+- Ensure the volume is correctly mounted
+- Check file permissions (font files should be readable)
+- Use absolute paths in environment variables
+
+## Why External Fonts?
+
+- **Smaller base image**: No need to embed large font files (~10MB per font)
+- **Flexibility**: Easily switch fonts without rebuilding the image
+- **Multi-language support**: Add support for any language by mounting appropriate fonts