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) }