label_printer/escpos/component.go
TT-392 4a0a055234 Squashed 'faxmachine/' content from commit d23200b
git-subtree-dir: faxmachine
git-subtree-split: d23200bcfdedb9f8cc57e6a3c65b5ef93fcbfd19
2024-09-12 20:45:00 +02:00

157 lines
4.1 KiB
Go

package escpos
import (
"errors"
"fmt"
"log/slog"
"strings"
"syscall"
"git.sr.ht/~guacamolie/faxmachine/escpos/protocol"
"github.com/djimenez/iconv-go"
)
type Component interface {
render(proto protocol.Protocol) ([][]byte, error)
}
type dataComponent struct {
data []byte
}
func (dc dataComponent) render(proto protocol.Protocol) ([][]byte, error) {
return [][]byte{dc.data}, nil
}
type groupComponent struct {
children []Component
}
func (gc groupComponent) render(proto protocol.Protocol) ([][]byte, error) {
var (
instructions [][]byte
errs []error
)
for _, child := range gc.children {
inst, err := child.render(proto)
instructions = append(instructions, inst...)
errs = append(errs, err)
}
return instructions, errors.Join(errs...)
}
// Text returns a Component that will print utf8-encoded text in an
// automatically chosen encoding. It will automatically switch between
// encodings mid-sentence if needed. The Component will return an error if the
// printer any part of the input string could not be converted.
//
// Text uses libc iconv to convert the text. Both musl libc and glibc are
// supported.
func Text(text string) Component {
return textComponent{text}
}
type textComponent struct {
text string
}
func (tc textComponent) render(proto protocol.Protocol) ([][]byte, error) {
if len(tc.text) == 0 {
return nil, nil
}
// musl libc uses '*' to represent characters it couldn't convert in the
// output string. This is more standard compliant than glibc, but does make
// it more difficult for us to detect if some characters couldn't be
// converted in a given encoding.
//
// So for musl libc we will be checking for '*' characters in the output to
// determine if the conversion failed. This means however that we can't
// have any '*' characters in our input, since this would mess up the
// detection.
//
// Luckily the first 127 characters are always the same on this printer for
// every possible encoding, meaning we can just directly print '*'
// characters without having to bother with encoding. The beauty of our
// US-centric world.
if before, after, found := strings.Cut(tc.text, "*"); found {
return groupComponent{
children: []Component{
textComponent{before},
dataComponent{[]byte("*")},
textComponent{after},
},
}.render(proto)
}
for _, encoding := range allEncodings {
if encoding.iconvEncoding == "" {
continue
}
converter, err := iconv.NewConverter("utf-8", encoding.iconvEncoding)
if err == syscall.EINVAL {
continue
} else if err != nil {
return nil, fmt.Errorf("failed to initialize converter from utf-8 to %s: %w", encoding.iconvEncoding, err)
}
defer converter.Close()
converted, err := converter.ConvertString(tc.text)
slog.Debug("iconv",
"utf8", tc.text,
slog.String(encoding.iconvEncoding, converted),
"error", err)
// glibc returns EILSEQ, musl libc uses '*' to represent invalid chars.
// Check for both.
if err == syscall.EILSEQ ||
(err == nil && strings.Index(converted, "*") != -1) {
// This encoding can't properly convert this string, try the next
// encoding.
continue
} else if err != nil {
return nil, fmt.Errorf("failed to convert text encoding: %w", err)
}
// We found a good encoding!
inst, err := proto.SelectCharacterCodeTable(encoding.page)
if err != nil {
// ...or not
continue
}
return [][]byte{
inst,
[]byte(converted),
}, nil
}
// None of the supported encoding convert this string. Try to see if it
// does work when splitting the string up into substrings.
// Find a midpoint in the string that is between two rune boundaries.
// TODO: Can this be made more correct? Not every rune boundary is a valid
// place to chop up a string.
halfwayIsh := 0
for i := range tc.text {
if i >= len(tc.text)/2 {
halfwayIsh = i
break
}
}
if halfwayIsh == 0 {
// We have exhausted all options to convert this, bail out
return nil, fmt.Errorf("device doesn't support %q characters", tc.text)
}
return groupComponent{
[]Component{
textComponent{tc.text[:halfwayIsh]},
textComponent{tc.text[halfwayIsh:]},
},
}.render(proto)
}