133 lines
3.4 KiB
Go
133 lines
3.4 KiB
Go
package host
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"image"
|
|
"image/png"
|
|
|
|
"aego/art"
|
|
)
|
|
|
|
const MaxPendingSaves = 8
|
|
|
|
type receipt struct {
|
|
key string
|
|
snapshot *art.Snapshot
|
|
writing bool
|
|
}
|
|
|
|
type Export struct {
|
|
Bytes []byte
|
|
Revision uint64
|
|
Key string
|
|
Receipt string
|
|
}
|
|
|
|
// Export captures bytes, frame metadata and the save checkpoint together.
|
|
// Recovery exports must use a receipt-free format ("recovery"). A receipt is
|
|
// acknowledged only after a durable writer succeeds or a user confirms a
|
|
// completed download; merely clicking an <a download> is not proof of saving.
|
|
func (d *Documents) Export(key, format string, columns int) (Export, error) {
|
|
e, err := d.lookup(key)
|
|
if err != nil {
|
|
return Export{}, err
|
|
}
|
|
if e.session.Info().Gesture {
|
|
return Export{}, fmt.Errorf("artstudio: finish the gesture before export")
|
|
}
|
|
if format == "aeart" && len(d.receipts) >= MaxPendingSaves {
|
|
return Export{}, fmt.Errorf("artstudio: too many unconfirmed saves")
|
|
}
|
|
snapshot, err := e.session.Document.Snapshot()
|
|
if err != nil {
|
|
return Export{}, err
|
|
}
|
|
var data []byte
|
|
switch format {
|
|
case "aeart", "recovery":
|
|
data, err = snapshot.Encode()
|
|
case "palette":
|
|
data, err = art.EncodePalette(snapshot.Info().Palette)
|
|
case "png":
|
|
var im *image.NRGBA
|
|
im, err = snapshot.Render(e.session.Frame, art.RenderOptions{Channel: e.session.Channel})
|
|
if err == nil {
|
|
var out bytes.Buffer
|
|
err = png.Encode(&out, im)
|
|
data = out.Bytes()
|
|
}
|
|
case "sheet", "sheet-json":
|
|
var sheet art.Sheet
|
|
sheet, err = snapshot.ExportSheet(columns, e.session.Channel)
|
|
if err == nil {
|
|
if format == "sheet" {
|
|
data = sheet.PNG
|
|
} else {
|
|
data, err = json.MarshalIndent(sheet, "", " ")
|
|
}
|
|
}
|
|
default:
|
|
return Export{}, fmt.Errorf("artstudio: unsupported export format %q", format)
|
|
}
|
|
if err != nil {
|
|
return Export{}, err
|
|
}
|
|
result := Export{Bytes: data, Revision: snapshot.Revision(), Key: e.key}
|
|
if format == "aeart" {
|
|
result.Receipt = d.next("save-")
|
|
d.receipts[result.Receipt] = receipt{key: e.key, snapshot: snapshot}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// BeginWrite validates a deferred file-picker result before opening its output.
|
|
// The receipt pins this document until Saved or CancelSave. Editing may continue;
|
|
// completion records exactly this snapshot rather than clearing newer changes.
|
|
func (d *Documents) BeginWrite(token string) error {
|
|
r, ok := d.receipts[token]
|
|
if !ok {
|
|
return fmt.Errorf("artstudio: expired save receipt")
|
|
}
|
|
e, err := d.lookup(r.key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if e.session.Info().Gesture {
|
|
return fmt.Errorf("artstudio: finish the active gesture before saving")
|
|
}
|
|
for _, other := range d.receipts {
|
|
if other.key == r.key && other.writing {
|
|
return fmt.Errorf("artstudio: another write is in progress")
|
|
}
|
|
}
|
|
if e.session.Document.Info().Revision != r.snapshot.Revision() {
|
|
return art.ErrConflict
|
|
}
|
|
r.writing = true
|
|
d.receipts[token] = r
|
|
return nil
|
|
}
|
|
|
|
func (d *Documents) Saved(token string) error {
|
|
r, ok := d.receipts[token]
|
|
if !ok {
|
|
return fmt.Errorf("artstudio: save receipt has expired")
|
|
}
|
|
if !r.writing {
|
|
return fmt.Errorf("artstudio: a completed write is required before acknowledging a save")
|
|
}
|
|
e, err := d.lookup(r.key)
|
|
if err != nil {
|
|
delete(d.receipts, token)
|
|
return err
|
|
}
|
|
if err = e.session.MarkSnapshotSaved(r.snapshot); err != nil {
|
|
return err
|
|
}
|
|
delete(d.receipts, token)
|
|
return nil
|
|
}
|
|
func (d *Documents) CancelSave(token string) { delete(d.receipts, token) }
|