diff --git a/.gitignore b/.gitignore index aa0ffa4..2437334 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.cbddlp *.ctb *.cws +*.fdg *.gf *.lgs *.lgs30 diff --git a/README.md b/README.md index 8619024..1aee24c 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,11 @@ The command line tool is designed to be used in a 'pipeline' style, for example: Options for '.cws': + Options for '.fdg': + + -e, --encryption-seed uint32 Specify a specific encryption seed + -v, --version int Specify the CTB version (2 or 3) (default 2) + Options for '.lgs': @@ -178,6 +183,7 @@ The command line tool is designed to be used in a 'pipeline' style, for example: photon Anycubic Photon Size: 1440x2560, 68x121 mm, Format: .photon photon0 Anycubic Photon Zero Size: 480x854, 55.4x98.6 mm, Format: .pw0 photons Anycubic Photon S Size: 1440x2560, 68x121 mm, Format: .pws + polaris Voxelab Polaris Size: 1440x2560, 68x121 mm, Format: .fdg s400 Kelant S400 Size: 2560x1600, 192x120 mm, Format: .zip shuffle Phrozen Shuffle Size: 1440x2560, 67.7x120 mm, Format: .zip sl1 Prusa SL1 Size: 1440x2560, 68x121 mm, Format: .sl1 diff --git a/cmd/uv3dp/main.go b/cmd/uv3dp/main.go index e54997d..bfb16e7 100644 --- a/cmd/uv3dp/main.go +++ b/cmd/uv3dp/main.go @@ -14,6 +14,7 @@ import ( _ "github.com/ezrec/uv3dp/ctb" _ "github.com/ezrec/uv3dp/cws" _ "github.com/ezrec/uv3dp/czip" + _ "github.com/ezrec/uv3dp/fdg" _ "github.com/ezrec/uv3dp/lgs" _ "github.com/ezrec/uv3dp/phz" _ "github.com/ezrec/uv3dp/pws" diff --git a/fdg/format.go b/fdg/format.go new file mode 100644 index 0000000..24cb082 --- /dev/null +++ b/fdg/format.go @@ -0,0 +1,624 @@ +// +// Copyright (c) 2020 Jason S. McMullan +// + +package fdg + +import ( + "fmt" + "image" + "io/ioutil" + "math/rand" + "sort" + "time" + + "encoding/binary" + + "github.com/go-restruct/restruct" + "github.com/spf13/pflag" + + "github.com/ezrec/uv3dp" +) + +const ( + defaultHeaderMagic = uint32(0xbd3c7ac8) + + defaultBottomLiftHeight = 5.0 + defaultBottomLiftSpeed = 300.0 + defaultLiftHeight = 5.0 + defaultLiftSpeed = 300.0 + defaultRetractSpeed = 300.0 + defaultRetractHeight = 6.0 + defaultBottomLightOff = 1.0 + defaultLightOff = 1.0 +) + +type fdgHeader struct { + Magic uint32 // 00: + Version uint32 // 04: Always '2' + LayerCount uint32 // 08: + BottomCount uint32 // 0c: Number of bottom layers + Projector uint32 // 10: 0 = CAST, 1 = LCD_X_MIRROR + BottomLayerCount uint32 // 14: Number of bottom layers + ResolutionX uint32 // 18: + ResolutionY uint32 // 1c: + LayerHeight float32 // 20: + LayerExposure float32 // 24: Layer exposure (in seconds) + BottomExposure float32 // 28: Bottom layers exporsure (in seconds) + PreviewHigh uint32 // 2c: Offset of the high-res preview + PreviewLow uint32 // 30: Offset of the high-res preview + LayerDefs uint32 // 34: Offset of the layer definitions + PrintTime uint32 // 38: Print time, in seconds + AntiAliasLevel uint32 // 3c: Always 1 for this format? + LightPWM uint16 // 40: + BottomLightPWM uint16 // 42: + _ [2]uint32 // 44: + HeightMM float32 // 4c: + BedSizeMM [3]float32 // 50: + EncryptionSeed uint32 // 5c: Encryption seed + AntiAliasDepth uint32 // 60: AntiAlias Level + EncryptionMode uint32 // 64: Possible encryption mode? (0x4c) + VolumeMilliliters float32 // 68: + WeightGrams float32 // 6c: + CostDollars float32 // 70: + MachineOffset uint32 // 74: Machine name offset + MachineSize uint32 // 78: Machine name length + BottomLightOffTime float32 // 7c: + LightOffTime float32 // 80: + _ uint32 // 84: + BottomLiftHeight float32 // 88: + BottomLiftSpeed float32 // 8c: + LiftHeight float32 // 90: + LiftSpeed float32 // 94: + RetractSpeed float32 // 98: + _ [7]uint32 // 9c: + Timestamp uint32 // b8: Minutes since Jan 1, 1970 UTC + ChiTuBoxVersion [4]byte // bc: major, minor, patch, release + _ [6]uint32 // c0: +} + +type fdgPreview struct { + ResolutionX uint32 // 00: + ResolutionY uint32 // 04: + ImageOffset uint32 // 08: + ImageLength uint32 // 0c: + _ [4]uint32 // 10: +} + +type fdgLayerDef struct { + LayerHeight float32 // 00: + LayerExposure float32 // 04: + LayerOffTime float32 // 08: + ImageOffset uint32 // 0c: + ImageLength uint32 // 10: + Unknown14 uint32 // 14: + InfoSize uint32 // 18: Size of image info + Unknown1c uint32 // 1c: + Unknown20 uint32 // 20: +} + +type fdgImageInfo struct { + LayerDef fdgLayerDef // 00: Repeat of the LayerDef information + TotalSize uint32 // 24: Total size of fdgImageInfo and Image data + LiftHeight float32 // 28: + LiftSpeed float32 // 2c: + Unknown30 uint32 // 30: Zero + Unknown34 uint32 // 34: Zero + RetractSpeed float32 // 38: + Unknown3c uint32 // 3c: Zero + Unknown40 uint32 // 40: Zero + Unknown44 uint32 // 44: Zero + Unknown48 uint32 // 48: Zero + Unknown4c uint32 // 4c: ?? + LightPWM float32 // 50: +} + +type Print struct { + uv3dp.Print + layerDef []fdgLayerDef + imageInfo [](*fdgImageInfo) + + rleMap map[uint32]([]byte) +} + +type Formatter struct { + *pflag.FlagSet + + EncryptionSeed uint32 + Version int +} + +func NewFormatter(suffix string) (cf *Formatter) { + flagSet := pflag.NewFlagSet(suffix, pflag.ContinueOnError) + flagSet.SetInterspersed(false) + + cf = &Formatter{ + FlagSet: flagSet, + } + + cf.Uint32VarP(&cf.EncryptionSeed, "encryption-seed", "e", 0, "Specify a specific encryption seed") + cf.IntVarP(&cf.Version, "version", "v", 2, "Specify the CTB version (2 or 3)") + + return +} + +// Save a uv3dp.Printable in CTB format +func (cf *Formatter) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err error) { + if cf.Version < 2 || cf.Version > 3 { + err = fmt.Errorf("unsupported version %v", cf.Version) + return + } + + size := printable.Size() + exp := printable.Exposure() + bot := printable.Bottom() + + // First, compute the rle images + type rleInfo struct { + offset uint32 + rle []byte + } + rleHash := map[uint64]rleInfo{} + + // Select an encryption seed + // A zero encryption seed is rejected by the printer, so check for that + seed := cf.EncryptionSeed + for seed == 0 { + seed = rand.Uint32() + } + + headerBase := uint32(0) + header := fdgHeader{ + Magic: defaultHeaderMagic, + Version: uint32(cf.Version), + EncryptionSeed: seed, + AntiAliasLevel: 1, + } + headerSize, _ := restruct.SizeOf(&header) + + // Add the preview images + var previewHuge fdgPreview + var previewTiny fdgPreview + previewSize, _ := restruct.SizeOf(&previewHuge) + + // Set up the RLE hash indexes + rleHashList := []uint64{} + + savePreview := func(base uint32, preview *fdgPreview, ptype uv3dp.PreviewType) uint32 { + pic, found := printable.Preview(ptype) + if !found { + return base + } + + base += uint32(previewSize) + size := pic.Bounds().Size() + if size == image.Pt(0, 0) { + return base + } + + // Collect preview images + rle, hash := rleEncodeRGB15(pic) + if len(rle) == 0 { + return base + } + + rleHash[hash] = rleInfo{offset: base, rle: rle} + rleHashList = append(rleHashList, hash) + + preview.ResolutionX = uint32(size.X) + preview.ResolutionY = uint32(size.Y) + preview.ImageOffset = rleHash[hash].offset + preview.ImageLength = uint32(len(rle)) + + return base + uint32(len(rle)) + } + + previewHugeBase := headerBase + uint32(headerSize) + + previewTinyBase := savePreview(previewHugeBase, &previewHuge, uv3dp.PreviewTypeHuge) + + machineBase := savePreview(previewTinyBase, &previewTiny, uv3dp.PreviewTypeTiny) + machine := "Voxelab Polaris" + machineSize := len(machine) + + layerDefBase := machineBase + uint32(machineSize) + + layerDef := make([]fdgLayerDef, size.Layers) + imageInfo := make([]fdgImageInfo, size.Layers) + layerDefSize, _ := restruct.SizeOf(&layerDef[0]) + + // And then all the layer images + layerPage := uint32(layerDefSize * size.Layers) + imageBase := layerDefBase + layerPage + totalOn := uint64(0) + + type layerInfo struct { + Z float32 + Exposure uv3dp.Exposure + Rle []byte + Hash uint64 + BitsOn uint + } + + doneMap := make([]chan layerInfo, size.Layers) + for n := 0; n < size.Layers; n++ { + doneMap[n] = make(chan layerInfo, 1) + } + + uv3dp.WithAllLayers(printable, func(p uv3dp.Printable, n int) { + rle, hash, bitsOn := rleEncodeGraymap(p.LayerImage(n)) + doneMap[n] <- layerInfo{ + Z: p.LayerZ(n), + Exposure: p.LayerExposure(n), + Rle: rle, + Hash: hash, + BitsOn: bitsOn, + } + close(doneMap[n]) + }) + + info_size, _ := restruct.SizeOf(&fdgImageInfo{}) + imageInfoSize := uint32(info_size) + if cf.Version < 3 { + imageInfoSize = 0 + } + + for n := 0; n < size.Layers; n++ { + info := <-doneMap[n] + if header.EncryptionSeed != 0 { + info.Hash = uint64(n) + info.Rle = cipher(header.EncryptionSeed, uint32(n), info.Rle) + } + _, ok := rleHash[info.Hash] + if !ok { + rleHash[info.Hash] = rleInfo{offset: imageBase + imageInfoSize, rle: info.Rle} + rleHashList = append(rleHashList, info.Hash) + imageBase = imageBase + imageInfoSize + uint32(len(info.Rle)) + } + + layerDef[n] = fdgLayerDef{ + LayerHeight: info.Z, + LayerExposure: info.Exposure.LightOnTime, + LayerOffTime: info.Exposure.LightOffTime, + ImageOffset: rleHash[info.Hash].offset, + ImageLength: uint32(len(info.Rle)), + InfoSize: imageInfoSize, + } + + if imageInfoSize > 0 { + imageInfo[n] = fdgImageInfo{ + LayerDef: layerDef[n], + TotalSize: uint32(len(info.Rle)) + imageInfoSize, + LiftHeight: info.Exposure.LiftHeight, + LiftSpeed: info.Exposure.LiftSpeed, + RetractSpeed: info.Exposure.RetractSpeed, + LightPWM: float32(info.Exposure.LightPWM), + } + } + + totalOn += uint64(info.BitsOn) + } + + // fdgHeader + header.BedSizeMM[0] = size.Millimeter.X + header.BedSizeMM[1] = size.Millimeter.Y + header.BedSizeMM[2] = 155.0 + header.HeightMM = size.LayerHeight * float32(size.Layers) + header.LayerHeight = size.LayerHeight + header.LayerExposure = exp.LightOnTime + header.BottomExposure = bot.Exposure.LightOnTime + header.LightOffTime = exp.LightOffTime + header.BottomCount = uint32(bot.Count) + header.ResolutionX = uint32(size.X) + header.ResolutionY = uint32(size.Y) + header.PreviewHigh = previewHugeBase + header.LayerDefs = layerDefBase + header.LayerCount = uint32(size.Layers) + header.PreviewLow = previewTinyBase + header.PrintTime = uint32(uv3dp.PrintDuration(printable) / time.Second) + header.Projector = 1 // LCD_X_MIRROR + + header.AntiAliasDepth = 4 + header.EncryptionMode = 0x4c + header.MachineOffset = machineBase + header.MachineSize = uint32(machineSize) + header.ChiTuBoxVersion[0] = 0 + header.ChiTuBoxVersion[1] = 0 + header.ChiTuBoxVersion[2] = 7 + header.ChiTuBoxVersion[3] = 1 + + if exp.LightPWM == 0 { + exp.LightPWM = 255 + } + + if bot.Exposure.LightPWM == 0 { + bot.Exposure.LightPWM = 255 + } + + header.LightPWM = uint16(exp.LightPWM) + header.BottomLightPWM = uint16(bot.Exposure.LightPWM) + + // fdgParam + header.BottomLayerCount = uint32(bot.Count) + header.BottomLiftSpeed = bot.Exposure.LiftSpeed + header.BottomLiftHeight = bot.Exposure.LiftHeight + header.LiftHeight = exp.LiftHeight + header.LiftSpeed = exp.LiftSpeed + header.RetractSpeed = exp.RetractSpeed + + if header.BottomLiftSpeed < 0 { + header.BottomLiftSpeed = defaultBottomLiftSpeed + } + if header.BottomLiftHeight < 0 { + header.BottomLiftHeight = defaultBottomLiftHeight + } + if header.LiftHeight < 0 { + header.LiftHeight = defaultLiftHeight + } + if header.LiftSpeed < 0 { + header.LiftSpeed = defaultLiftSpeed + } + if header.RetractSpeed < 0 { + header.RetractSpeed = defaultRetractSpeed + } + + // Compute total cubic millimeters (== milliliters) of all the on pixels + bedArea := float64(header.BedSizeMM[0] * header.BedSizeMM[1]) + bedPixels := float64(header.ResolutionX) * float64(header.ResolutionY) + pixelVolume := float64(header.LayerHeight) * bedArea / bedPixels * 200.0 + header.VolumeMilliliters = float32(float64(totalOn) * pixelVolume / 1000.0) + header.WeightGrams = header.VolumeMilliliters * 1.1 // Just a guess on resin density + header.CostDollars = header.WeightGrams * 0.1 + + header.BottomLightOffTime = bot.Exposure.LightOffTime + header.LightOffTime = exp.LightOffTime + header.BottomLayerCount = header.BottomCount + header.Timestamp = uint32(time.Now().Unix() / 60) + + // Collect file data + fileData := map[int][]byte{} + + fileData[int(headerBase)], _ = restruct.Pack(binary.LittleEndian, &header) + + for n, layer := range layerDef { + base := int(layerDefBase) + layerDefSize*n + fileData[base], _ = restruct.Pack(binary.LittleEndian, &layer) + } + + fileData[int(previewHugeBase)], _ = restruct.Pack(binary.LittleEndian, &previewHuge) + fileData[int(previewTinyBase)], _ = restruct.Pack(binary.LittleEndian, &previewTiny) + + fileData[int(machineBase)] = ([]byte)(machine) + + for _, hash := range rleHashList { + info := rleHash[hash] + fileData[int(info.offset)] = info.rle + } + + if imageInfoSize > 0 { + for _, info := range imageInfo { + data, _ := restruct.Pack(binary.LittleEndian, &info) + fileData[int(info.LayerDef.ImageOffset-imageInfoSize)] = data + } + } + + // Sort the file data + fileIndex := []int{} + for key := range fileData { + fileIndex = append(fileIndex, key) + } + + sort.Ints(fileIndex) + + offset := 0 + for _, base := range fileIndex { + // Pad as needed + writer.Write(make([]byte, base-offset)) + + // Write the data + data := fileData[base] + delete(fileData, base) + + writer.Write(data) + + // Set up next offset + offset = base + len(data) + } + + return +} + +func cipher(seed uint32, slice uint32, in []byte) (out []byte) { + if seed == 0 { + out = in + } else { + kr := NewKeyring(seed, slice) + + for _, c := range in { + out = append(out, c^kr.Next()) + } + } + + return +} + +func (cf *Formatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) { + // Collect file + data, err := ioutil.ReadAll(file) + if err != nil { + return + } + + prop := uv3dp.Properties{ + Preview: make(map[uv3dp.PreviewType]image.Image), + } + + header := fdgHeader{} + err = restruct.Unpack(data, binary.LittleEndian, &header) + if err != nil { + return + } + + if header.Magic != defaultHeaderMagic { + err = fmt.Errorf("Unknown header magic: 0x%08x", header.Magic) + return + } + + // Collect previews + previewTable := []struct { + previewType uv3dp.PreviewType + previewOffset uint32 + }{ + {previewType: uv3dp.PreviewTypeTiny, previewOffset: header.PreviewLow}, + {previewType: uv3dp.PreviewTypeHuge, previewOffset: header.PreviewHigh}, + } + + for _, item := range previewTable { + if item.previewOffset == 0 { + continue + } + + var preview fdgPreview + err = restruct.Unpack(data[item.previewOffset:], binary.LittleEndian, &preview) + if err != nil { + return + } + + addr := preview.ImageOffset + size := preview.ImageLength + + bounds := image.Rect(0, 0, int(preview.ResolutionX), int(preview.ResolutionY)) + var pic image.Image + pic, err = rleDecodeRGB15(bounds, data[addr:addr+size]) + if err != nil { + return + } + + prop.Preview[item.previewType] = pic + } + + seed := header.EncryptionSeed + + // Collect layers + rleMap := make(map[uint32]([]byte)) + + layerDef := make([]fdgLayerDef, header.LayerCount) + + imageInfo := make([](*fdgImageInfo), header.LayerCount) + + layerDefSize := uint32(9 * 4) + for n := uint32(0); n < header.LayerCount; n++ { + offset := header.LayerDefs + layerDefSize*n + err = restruct.Unpack(data[offset:], binary.LittleEndian, &layerDef[n]) + if err != nil { + return + } + + addr := layerDef[n].ImageOffset + size := layerDef[n].ImageLength + + rleMap[addr] = cipher(seed, n, data[addr:addr+size]) + + infoSize := layerDef[n].InfoSize + if header.Version >= 3 && infoSize > 0 { + info := &fdgImageInfo{} + err = restruct.Unpack(data[addr-infoSize:addr], binary.LittleEndian, info) + if err != nil { + imageInfo[n] = info + } + } + } + + size := &prop.Size + size.Millimeter.X = header.BedSizeMM[0] + size.Millimeter.Y = header.BedSizeMM[1] + + size.X = int(header.ResolutionX) + size.Y = int(header.ResolutionY) + + size.Layers = int(header.LayerCount) + size.LayerHeight = header.LayerHeight + + exp := &prop.Exposure + exp.LightOnTime = header.LayerExposure + exp.LightOffTime = header.LightOffTime + exp.LightPWM = uint8(header.LightPWM) + + bot := &prop.Bottom + bot.Count = int(header.BottomCount) + bot.Exposure.LightOnTime = header.BottomExposure + bot.Exposure.LightOffTime = header.LightOffTime + bot.Exposure.LightPWM = uint8(header.BottomLightPWM) + + bot.Count = int(header.BottomLayerCount) + bot.Exposure.LiftHeight = header.BottomLiftHeight + bot.Exposure.LiftSpeed = header.BottomLiftSpeed + bot.Exposure.LightOffTime = header.BottomLightOffTime + bot.Exposure.RetractSpeed = header.RetractSpeed + bot.Exposure.RetractHeight = defaultRetractHeight + + exp.LiftHeight = header.LiftHeight + exp.LiftSpeed = header.LiftSpeed + exp.LightOffTime = header.LightOffTime + exp.RetractSpeed = header.RetractSpeed + exp.RetractHeight = defaultRetractHeight + + fdg := &Print{ + Print: uv3dp.Print{Properties: prop}, + layerDef: layerDef, + imageInfo: imageInfo, + rleMap: rleMap, + } + + printable = fdg + + return +} + +func (fdg *Print) LayerImage(index int) (layerImage *image.Gray) { + layerDef := fdg.layerDef[index] + + // Update per-layer info + layerImage, err := rleDecodeGraymap(fdg.Bounds(), fdg.rleMap[layerDef.ImageOffset]) + if err != nil { + panic(err) + } + + return +} + +func (fdg *Print) LayerExposure(index int) (exposure uv3dp.Exposure) { + layerDef := fdg.layerDef[index] + + if index < fdg.Bottom().Count { + exposure = fdg.Bottom().Exposure + } else { + exposure = fdg.Exposure() + } + + if layerDef.LayerExposure > 0.0 { + exposure.LightOnTime = layerDef.LayerExposure + } + + if layerDef.LayerOffTime > 0.0 { + exposure.LightOffTime = layerDef.LayerOffTime + } + + // See if we have per-layer overrides + info := fdg.imageInfo[index] + if info != nil { + exposure.LightOnTime = info.LayerDef.LayerExposure + exposure.LightOffTime = info.LayerDef.LayerOffTime + exposure.LightPWM = uint8(info.LightPWM) + exposure.LiftHeight = info.LiftHeight + exposure.LiftSpeed = info.LiftSpeed + exposure.RetractSpeed = info.RetractSpeed + } + + return +} + +func (fdg *Print) LayerZ(index int) (z float32) { + z = fdg.layerDef[index].LayerHeight + return +} diff --git a/fdg/init.go b/fdg/init.go new file mode 100644 index 0000000..4f2453b --- /dev/null +++ b/fdg/init.go @@ -0,0 +1,24 @@ +// +// Copyright (c) 2020 Jason S. McMullan +// + +// Package fdg handle input and output of Voxelab Polaris printers +package fdg + +import ( + "github.com/ezrec/uv3dp" +) + +var ( + machines_fdg = map[string]uv3dp.Machine{ + "polaris": {Vendor: "Voxelab", Model: "Polaris", Size: uv3dp.MachineSize{1440, 2560, 68.04, 120.96}}, + } +) + +func init() { + newFormatter := func(suffix string) (format uv3dp.Formatter) { return NewFormatter(suffix) } + + uv3dp.RegisterFormatter(".fdg", newFormatter) + + uv3dp.RegisterMachines(machines_fdg, ".fdg") +} diff --git a/fdg/keyring.go b/fdg/keyring.go new file mode 100644 index 0000000..7feb054 --- /dev/null +++ b/fdg/keyring.go @@ -0,0 +1,40 @@ +package fdg + +type Keyring struct { + Init uint32 + Key uint32 + index int +} + +// Key encoding was very similar to, but had different constants than: +// https://github.com/cbiffle/catibo/blob/master/doc/cbddlp-ctb.adoc +func NewKeyring(seed uint32, slice uint32) (kr *Keyring) { + init := (seed - 0x1dcb76c3) ^ 0x257e2431 + key := init * 0x82391efd * (slice ^ 0x110bdacd) + + kr = &Keyring{ + Init: init, + Key: key, + } + + return +} + +func (kr *Keyring) Next() (k byte) { + k = byte(kr.Key >> (8 * kr.index)) + kr.index += 1 + if kr.index&3 == 0 { + kr.Key += kr.Init + kr.index = 0 + } + + return +} + +func (kr *Keyring) Read(buff []byte) (size int, err error) { + for n := range buff { + buff[n] = kr.Next() + } + + return +} diff --git a/fdg/keyring_test.go b/fdg/keyring_test.go new file mode 100644 index 0000000..3dd130f --- /dev/null +++ b/fdg/keyring_test.go @@ -0,0 +1,17 @@ +package fdg + +import ( + "testing" +) + +func TestKeyring(t *testing.T) { + kr := NewKeyring(0xdcbe9950, 20) + + if kr.Init != 0x9b8d06bc { + t.Fatalf("Init: expected %#v, got %#v", uint32(0x9b8d06bc), kr.Init) + } + + if kr.Key != 0x4749bbec { + t.Fatalf("Key: expected %#v, got %#v", uint32(0x4749bbec), kr.Key) + } +} diff --git a/fdg/rle.go b/fdg/rle.go new file mode 100644 index 0000000..6f7a8ab --- /dev/null +++ b/fdg/rle.go @@ -0,0 +1,231 @@ +// +// Copyright (c) 2020 Jason S. McMullan +// + +package fdg + +import ( + "fmt" + "image" + "image/color" + + "encoding/binary" + "hash/crc64" +) + +const ( + rle8EncodingLimit = 125 // Yah, I know. Feels weird. But required. + rle16EncodingLimit = 0xfff +) + +var tab64 *crc64.Table + +func hash64(data []byte) (hash uint64) { + if tab64 == nil { + tab64 = crc64.MakeTable(crc64.ECMA) + } + + hash = crc64.Checksum(data, tab64) + return +} + +func rleEncodeGraymap(bm image.Image) (rle []byte, hash uint64, bitsOn uint) { + base := bm.Bounds().Min + size := bm.Bounds().Size() + + addRep := func(gray7 uint8, stride int) { + if gray7 > 0 { + bitsOn += uint(stride) + } + + rle = append(rle, gray7|0x80) + stride-- + for done := 0; done < stride; { + todo := 0x7d + if (stride - done) < todo { + todo = (stride - done) + } + + rle = append(rle, byte(todo)) + + done += todo + } + } + + color := byte(0xff) + var stride int + + for y := 0; y < size.Y; y++ { + var grey7 uint8 + for x := 0; x < size.X; x++ { + c := bm.At(base.X+x, base.Y+y) + r, g, b, _ := c.RGBA() + // 7 bits per pixel (clamped to 0..0x7c) + grey7 = uint8(uint16(r|g|b)>>9) & 0x7f + if grey7 > 0x7c { + grey7 = 0x7c + } + + switch { + case color == 0xff: + color = grey7 + stride = 1 + case grey7 != color || x == size.X/2: + addRep(color, stride) + color = grey7 + stride = 1 + default: + stride++ + } + } + addRep(color, stride) + color = 0xff + } + + hash = hash64(rle) + + return +} + +func rleDecodeGraymap(bounds image.Rectangle, rle []byte) (gm *image.Gray, err error) { + limit := bounds.Size().X * bounds.Size().Y + pix := make([]byte, limit) + + var index int + var lastColor byte + + for n := 0; n < len(rle); n++ { + code := rle[n] + if (code & 0x80) == 0x80 { + // Convert from 0..124 to 8bpp + lastColor = ((code & 0x7f) << 1) | (code & 1) + if lastColor >= 0xfc { + // Make 'white' actually white + lastColor = 0xff + } + if index < limit { + pix[index] = lastColor + } + index++ + } else { + for i := 0; i < int(code); i++ { + if index < limit { + pix[index] = lastColor + } + index++ + } + } + } + + if index != limit { + err = fmt.Errorf("expected %v pixels, saw %v", limit, index) + return + } + + gm = &image.Gray{ + Pix: pix, + Stride: bounds.Size().X, + Rect: bounds, + } + + return +} + +func color5to8(c5 uint16) (c8 uint8) { + return uint8((c5 << 3) | (c5 >> 2)) +} + +func color16to5(c16 uint32) (c5 uint16) { + return uint16((c16 >> (16 - 5)) & 0x1f) +} + +const repeatRGB15Mask = uint16(1 << 5) + +func rleRGB15(color15 uint16, rep int) (rle []byte) { + switch rep { + case 0: + // pass... + case 1: + data := [2]byte{} + binary.LittleEndian.PutUint16(data[0:2], color15) + rle = data[:] + case 2: + data := [4]byte{} + binary.LittleEndian.PutUint16(data[0:2], color15) + binary.LittleEndian.PutUint16(data[2:4], color15) + rle = data[:] + default: + data := [4]byte{} + binary.LittleEndian.PutUint16(data[0:2], color15|repeatRGB15Mask) + binary.LittleEndian.PutUint16(data[2:4], uint16(rep-1)|(0x3000)) + rle = data[:] + } + + return +} + +func rleEncodeRGB15(bm image.Image) (rle []byte, hash uint64) { + base := bm.Bounds().Min + size := bm.Bounds().Size() + + color15 := uint16(0) + rep := 0 + for y := 0; y < size.Y; y++ { + for x := 0; x < size.X; x++ { + ncR, ncG, ncB, _ := bm.At(base.X+x, base.Y+y).RGBA() + ncolor15 := color16to5(ncB) + ncolor15 |= color16to5(ncG) << 6 + ncolor15 |= color16to5(ncR) << 11 + if ncolor15 == color15 { + rep++ + if rep == rle16EncodingLimit { + rle = append(rle, rleRGB15(color15, rep)...) + rep = 0 + } + } else { + rle = append(rle, rleRGB15(color15, rep)...) + color15 = ncolor15 + rep = 1 + } + } + } + + rle = append(rle, rleRGB15(color15, rep)...) + + hash = hash64(rle) + + return +} + +func rleDecodeRGB15(bounds image.Rectangle, rle []byte) (view *image.RGBA, err error) { + view = image.NewRGBA(bounds) + + y := bounds.Min.Y + x := bounds.Min.X + for n := 0; n < len(rle); n += 2 { + color16 := binary.LittleEndian.Uint16(rle[n : n+2]) + repeat := int(1) + if (color16 & repeatRGB15Mask) != 0 { + n += 2 + repeat += int(binary.LittleEndian.Uint16(rle[n:n+2]) & 0xfff) + } + + colorRgba := color.RGBA{ + R: color5to8((color16 >> 11) & 0x1f), + G: color5to8((color16 >> 6) & 0x1f), + B: color5to8((color16 >> 0) & 0x1f), + A: 255, + } + + for r := 0; r < repeat; r++ { + view.Set(x, y, colorRgba) + x++ + if x == bounds.Max.X { + x = bounds.Min.X + y++ + } + } + } + + return +} diff --git a/fdg/rle_test.go b/fdg/rle_test.go new file mode 100644 index 0000000..fc142ee --- /dev/null +++ b/fdg/rle_test.go @@ -0,0 +1,106 @@ +package fdg + +import ( + "image" + "testing" +) + +func TestRleEncodeGraymap(t *testing.T) { + rect := image.Rect(0, 0, 8, 21) + gray := &image.Gray{ + Pix: []uint8{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 00 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 08 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 10 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 18 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 20 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 28 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 30 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 38 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 40 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 48 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 50 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 58 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 60 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 68 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 70 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 78 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 80 + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 88 + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, // 90 + 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // a0 + }, + Stride: rect.Size().X, + Rect: rect, + } + + out_rle := []byte{ + 0xfc, 0x3, 0xfc, 0x3, 0x80, 0x3, 0x80, 0x3, + 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, + 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, + 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, + 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, + 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, + 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, + 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, + 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, 0xfc, 0x3, + 0xfc, 0x3, 0xfc, 0x80, 0x1, 0xfc, 0xfc, 0x80, + 0xfc, 0x80, 0xfc, 0x80, 0xfc, 0x80, 0x80, 0x3, 0x80, 0x3, + } + + out_hash := uint64(0xb323ff9e2d9a0478) + out_bits := uint(146) + + rle, hash, bits := rleEncodeGraymap(gray) + + if bits != out_bits { + t.Errorf("expected %v, got %v", out_bits, bits) + } + + if out_hash != hash { + t.Errorf("expected %#v, got %#v", out_hash, hash) + } + + if len(rle) != len(out_rle) { + t.Logf("%#v", rle) + t.Fatalf("expected %v, got %v", len(out_rle), len(rle)) + } + + for n, b := range out_rle { + if rle[n] != b { + t.Errorf("%v: expected %#v, got %#v", n, b, rle[n]) + } + } + + // All empty + rect = image.Rect(0, 0, 127, 4) + gray.Rect = rect + gray.Stride = rect.Size().X + gray.Pix = make([]byte, rect.Size().X*rect.Size().Y) + + rle, hash, bits = rleEncodeGraymap(gray) + + out_rle = []byte{0x80, 0x3e, 0x80, 0x3f, 0x80, 0x3e, 0x80, 0x3f, 0x80, 0x3e, 0x80, 0x3f, 0x80, 0x3e, 0x80, 0x3f} + out_hash = uint64(0xfc266e9b417b66dc) + out_bits = 0 + + if out_bits != bits { + t.Errorf("expected %v, got %v", out_bits, bits) + } + + if out_hash != hash { + t.Errorf("expected %#v, got %#v", out_hash, hash) + } + + if len(rle) != len(out_rle) { + t.Fatalf("expected %v, got %v %#v", len(out_rle), len(rle), rle) + } + + for n, b := range out_rle { + if rle[n] != b { + t.Errorf("%v: expected %#v, got %#v", n, b, rle[n]) + } + } + +}