4a0a055234
git-subtree-dir: faxmachine git-subtree-split: d23200bcfdedb9f8cc57e6a3c65b5ef93fcbfd19
157 lines
4.1 KiB
Go
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)
|
|
}
|