diff --git a/d2common/d2fileformats/d2font/doc.go b/d2common/d2fileformats/d2font/doc.go new file mode 100644 index 00000000..28f60bf9 --- /dev/null +++ b/d2common/d2fileformats/d2font/doc.go @@ -0,0 +1,3 @@ +// Package d2font contains logic for loading and processing +// d2 fonts +package d2font diff --git a/d2common/d2fileformats/d2font/font.go b/d2common/d2fileformats/d2font/font.go new file mode 100644 index 00000000..7789e277 --- /dev/null +++ b/d2common/d2fileformats/d2font/font.go @@ -0,0 +1,235 @@ +package d2font + +import ( + "fmt" + "image/color" + "strings" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" +) + +const ( + knownSignature = "Woo!\x01" +) + +const ( + signatureBytesCount = 5 + unknownHeaderBytesCount = 7 + unknown1BytesCount = 1 + unknown2BytesCount = 3 + unknown3BytesCount = 4 +) + +type fontGlyph struct { + unknown1 []byte + unknown2 []byte + unknown3 []byte + frame int + width int + height int +} + +func (fg *fontGlyph) setHeight(h int) { + fg.height = h +} + +// Font represents a displayable font +type Font struct { + unknownHeaderBytes []byte + sheet d2interface.Animation + table []byte + glyphs map[rune]*fontGlyph + color color.Color +} + +// Load loads a new font from byte slice +func Load(data []byte) (*Font, error) { + sr := d2datautils.CreateStreamReader(data) + + signature, err := sr.ReadBytes(signatureBytesCount) + if err != nil { + return nil, err + } + + if string(signature) != knownSignature { + return nil, fmt.Errorf("invalid font table format") + } + + font := &Font{ + table: data, + color: color.White, + } + + font.unknownHeaderBytes, err = sr.ReadBytes(unknownHeaderBytesCount) + if err != nil { + return nil, err + } + + err = font.initGlyphs(sr) + if err != nil { + return nil, err + } + + return font, nil +} + +// SetBackground sets font's background +func (f *Font) SetBackground(sheet d2interface.Animation) { + f.sheet = sheet + + // recalculate max height + _, h := f.sheet.GetFrameBounds() + + for i := range f.glyphs { + f.glyphs[i].setHeight(h) + } +} + +// SetColor sets the fonts color +func (f *Font) SetColor(c color.Color) { + f.color = c +} + +// GetTextMetrics returns the dimensions of the Font element in pixels +func (f *Font) GetTextMetrics(text string) (width, height int) { + var ( + lineWidth int + lineHeight int + ) + + for _, c := range text { + if c == '\n' { + width = d2math.MaxInt(width, lineWidth) + height += lineHeight + lineWidth = 0 + lineHeight = 0 + } else if glyph, ok := f.glyphs[c]; ok { + lineWidth += glyph.width + lineHeight = d2math.MaxInt(lineHeight, glyph.height) + } + } + + width = d2math.MaxInt(width, lineWidth) + height += lineHeight + + return width, height +} + +// RenderText prints a text using its configured style on a Surface (multi-lines are left-aligned, use label otherwise) +func (f *Font) RenderText(text string, target d2interface.Surface) error { + f.sheet.SetColorMod(f.color) + + lines := strings.Split(text, "\n") + + for _, line := range lines { + var ( + lineHeight int + lineLength int + ) + + for _, c := range line { + glyph, ok := f.glyphs[c] + if !ok { + continue + } + + if err := f.sheet.SetCurrentFrame(glyph.frame); err != nil { + return err + } + + f.sheet.Render(target) + + lineHeight = d2math.MaxInt(lineHeight, glyph.height) + lineLength++ + + target.PushTranslation(glyph.width, 0) + } + + target.PopN(lineLength) + target.PushTranslation(0, lineHeight) + } + + target.PopN(len(lines)) + + return nil +} + +func (f *Font) initGlyphs(sr *d2datautils.StreamReader) error { + glyphs := make(map[rune]*fontGlyph) + + for i := 12; i < len(f.table); i += 14 { + code, err := sr.ReadUInt16() + if err != nil { + return err + } + + var glyph fontGlyph + + // two bytes of 0 + glyph.unknown1, err = sr.ReadBytes(unknown1BytesCount) + if err != nil { + return err + } + + width, err := sr.ReadByte() + if err != nil { + return err + } + + glyph.width = int(width) + + height, err := sr.ReadByte() + if err != nil { + return err + } + + glyph.height = int(height) + + // 1, 0, 0 + glyph.unknown2, err = sr.ReadBytes(unknown2BytesCount) + if err != nil { + return err + } + + frame, err := sr.ReadUInt16() + if err != nil { + return err + } + + glyph.frame = int(frame) + + // 1, 0, 0, character code repeated, and further 0. + glyph.unknown3, err = sr.ReadBytes(unknown3BytesCount) + if err != nil { + return err + } + + glyphs[rune(code)] = &glyph + } + + f.glyphs = glyphs + + return nil +} + +// Marshal encodes font back into byte slice +func (f *Font) Marshal() []byte { + sw := d2datautils.CreateStreamWriter() + + sw.PushBytes([]byte("Woo!\x01")...) + sw.PushBytes(f.unknownHeaderBytes...) + + for c, i := range f.glyphs { + sw.PushUint16(uint16(c)) + sw.PushBytes(i.unknown1...) + sw.PushBytes(byte(i.width)) + sw.PushBytes(byte(i.height)) + sw.PushBytes(i.unknown2...) + sw.PushUint16(uint16(i.frame)) + sw.PushBytes(i.unknown3...) + } + + return sw.GetBytes() +} diff --git a/d2core/d2asset/asset_manager.go b/d2core/d2asset/asset_manager.go index c55bf6af..d3c8cb6f 100644 --- a/d2core/d2asset/asset_manager.go +++ b/d2core/d2asset/asset_manager.go @@ -2,7 +2,6 @@ package d2asset import ( "fmt" - "image/color" "io" "io/ioutil" "path/filepath" @@ -19,6 +18,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2records" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2font" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" @@ -209,11 +209,11 @@ func (am *AssetManager) LoadComposite(baseType d2enum.ObjectType, token, palette } // LoadFont loads a font the resource files -func (am *AssetManager) LoadFont(tablePath, spritePath, palettePath string) (*Font, error) { +func (am *AssetManager) LoadFont(tablePath, spritePath, palettePath string) (*d2font.Font, error) { cachePath := fmt.Sprintf("%s;%s;%s", tablePath, spritePath, palettePath) if cached, found := am.fonts.Retrieve(cachePath); found { - return cached.(*Font), nil + return cached.(*d2font.Font), nil } sheet, err := am.LoadAnimation(spritePath, palettePath) @@ -226,18 +226,15 @@ func (am *AssetManager) LoadFont(tablePath, spritePath, palettePath string) (*Fo return nil, err } - if string(tableData[:5]) != "Woo!\x01" { - return nil, fmt.Errorf("invalid font table format: %s", tablePath) - } - am.Debugf(fmtLoadFont, tablePath, spritePath, palettePath) - font := &Font{ - table: tableData, - sheet: sheet, - color: color.White, + font, err := d2font.Load(tableData) + if err != nil { + return nil, fmt.Errorf("error while loading font table %s: %v", tablePath, err) } + font.SetBackground(sheet) + err = am.fonts.Insert(cachePath, font, defaultCacheEntryWeight) return font, err diff --git a/d2core/d2asset/font.go b/d2core/d2asset/font.go deleted file mode 100644 index 5d898272..00000000 --- a/d2core/d2asset/font.go +++ /dev/null @@ -1,121 +0,0 @@ -package d2asset - -import ( - "encoding/binary" - "image/color" - "strings" - - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" -) - -type fontGlyph struct { - frame int - width int - height int -} - -// Font represents a displayable font -type Font struct { - sheet d2interface.Animation - table []byte - glyphs map[rune]fontGlyph - color color.Color -} - -// SetColor sets the fonts color -func (f *Font) SetColor(c color.Color) { - f.color = c -} - -// GetTextMetrics returns the dimensions of the Font element in pixels -func (f *Font) GetTextMetrics(text string) (width, height int) { - if f.glyphs == nil { - f.initGlyphs() - } - - var ( - lineWidth int - lineHeight int - ) - - for _, c := range text { - if c == '\n' { - width = d2math.MaxInt(width, lineWidth) - height += lineHeight - lineWidth = 0 - lineHeight = 0 - } else if glyph, ok := f.glyphs[c]; ok { - lineWidth += glyph.width - lineHeight = d2math.MaxInt(lineHeight, glyph.height) - } - } - - width = d2math.MaxInt(width, lineWidth) - height += lineHeight - - return width, height -} - -// RenderText prints a text using its configured style on a Surface (multi-lines are left-aligned, use label otherwise) -func (f *Font) RenderText(text string, target d2interface.Surface) error { - if f.glyphs == nil { - f.sheet.BindRenderer(target.Renderer()) - f.initGlyphs() - } - - f.sheet.SetColorMod(f.color) - - lines := strings.Split(text, "\n") - - for _, line := range lines { - var ( - lineHeight int - lineLength int - ) - - for _, c := range line { - glyph, ok := f.glyphs[c] - if !ok { - continue - } - - if err := f.sheet.SetCurrentFrame(glyph.frame); err != nil { - return err - } - - f.sheet.Render(target) - - lineHeight = d2math.MaxInt(lineHeight, glyph.height) - lineLength++ - - target.PushTranslation(glyph.width, 0) - } - - target.PopN(lineLength) - target.PushTranslation(0, lineHeight) - } - - target.PopN(len(lines)) - - return nil -} - -func (f *Font) initGlyphs() { - _, maxCharHeight := f.sheet.GetFrameBounds() - - glyphs := make(map[rune]fontGlyph) - - for i := 12; i < len(f.table); i += 14 { - code := rune(binary.LittleEndian.Uint16(f.table[i : i+2])) - - var glyph fontGlyph - glyph.frame = int(binary.LittleEndian.Uint16(f.table[i+8 : i+10])) - glyph.width = int(f.table[i+3]) - glyph.height = maxCharHeight - - glyphs[code] = glyph - } - - f.glyphs = glyphs -} diff --git a/d2core/d2gui/label.go b/d2core/d2gui/label.go index 03427f79..3130d5d1 100644 --- a/d2core/d2gui/label.go +++ b/d2core/d2gui/label.go @@ -4,8 +4,8 @@ import ( "image/color" "time" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2font" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" ) // Constants defining the main shades of basic colors @@ -25,7 +25,7 @@ type Label struct { renderer d2interface.Renderer text string - font *d2asset.Font + font *d2font.Font surface d2interface.Surface color color.RGBA hoverColor color.RGBA @@ -35,7 +35,7 @@ type Label struct { blinkTimer time.Time } -func createLabel(renderer d2interface.Renderer, text string, font *d2asset.Font, col color.RGBA) (*Label, error) { +func createLabel(renderer d2interface.Renderer, text string, font *d2font.Font, col color.RGBA) (*Label, error) { label := &Label{ font: font, renderer: renderer, diff --git a/d2core/d2gui/layout.go b/d2core/d2gui/layout.go index b6c487b5..25ad1af0 100644 --- a/d2core/d2gui/layout.go +++ b/d2core/d2gui/layout.go @@ -4,6 +4,7 @@ import ( "errors" "image/color" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2font" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" @@ -532,7 +533,7 @@ func (l *Layout) createButton(renderer d2interface.Renderer, text string, return button, nil } -func (l *Layout) loadFont(fontStyle FontStyle) (*d2asset.Font, error) { +func (l *Layout) loadFont(fontStyle FontStyle) (*d2font.Font, error) { config := getFontStyleConfig(fontStyle) if config == nil { return nil, errors.New("invalid font style") diff --git a/d2core/d2ui/label.go b/d2core/d2ui/label.go index 9026697a..912b0788 100644 --- a/d2core/d2ui/label.go +++ b/d2core/d2ui/label.go @@ -5,7 +5,7 @@ import ( "regexp" "strings" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2font" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" @@ -19,7 +19,7 @@ type Label struct { *BaseWidget text string Alignment HorizontalAlign - font *d2asset.Font + font *d2font.Font Color map[int]color.Color backgroundColor color.Color