Repository: ezrec/uv3dp
Pull request #101 (2020-09-07 04:36:39)

Issue 91 estimated print time


Patch:
diff --git a/cbddlp/format.go b/cbddlp/format.go
index 7f0a5e4..7f59581 100644
--- a/cbddlp/format.go
+++ b/cbddlp/format.go
@@ -9,6 +9,7 @@ import (
 	"image"
 	"io/ioutil"
 	"sort"
+	"time"
 
 	"encoding/binary"
 
@@ -96,9 +97,9 @@ type cbddlpLayerDef struct {
 	_             [4]uint32 // 14:
 }
 
-type CbdDlp struct {
-	properties uv3dp.Properties
-	layerDef   []cbddlpLayerDef
+type Print struct {
+	uv3dp.Print
+	layerDef []cbddlpLayerDef
 
 	rleMap map[uint32]([]([]byte))
 }
@@ -108,14 +109,14 @@ func align4(in uint32) (out uint32) {
 	return
 }
 
-type CbddlpFormatter struct {
+type Formatter struct {
 	*pflag.FlagSet
 
 	Version   int // Version of file to use, one of [1,2]
 	AntiAlias int // AntiAlias level, one of [1,2,4,8]
 }
 
-func NewCbddlpFormatter(suffix string) (cf *CbddlpFormatter) {
+func NewFormatter(suffix string) (cf *Formatter) {
 	var version int
 	var antialias int
 
@@ -134,7 +135,7 @@ func NewCbddlpFormatter(suffix string) (cf *CbddlpFormatter) {
 	flagSet := pflag.NewFlagSet(suffix, pflag.ContinueOnError)
 	flagSet.SetInterspersed(false)
 
-	cf = &CbddlpFormatter{
+	cf = &Formatter{
 		FlagSet:   flagSet,
 		Version:   version,
 		AntiAlias: antialias,
@@ -147,7 +148,7 @@ func NewCbddlpFormatter(suffix string) (cf *CbddlpFormatter) {
 }
 
 // Save a uv3dp.Printable in CBD DLP format
-func (cf *CbddlpFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err error) {
+func (cf *Formatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err error) {
 	switch cf.Version {
 	case 1:
 		if cf.AntiAlias != 1 {
@@ -164,11 +165,9 @@ func (cf *CbddlpFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err e
 		return
 	}
 
-	properties := p.Properties()
-
-	size := &properties.Size
-	exp := &properties.Exposure
-	bot := &properties.Bottom
+	size := p.Size()
+	exp := p.Exposure()
+	bot := p.Bottom()
 
 	// First, compute the rle images
 	type rleInfo struct {
@@ -193,7 +192,7 @@ func (cf *CbddlpFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err e
 	rleHashList := []uint64{}
 
 	savePreview := func(base uint32, preview *cbddlpPreview, ptype uv3dp.PreviewType) uint32 {
-		pic, found := properties.Preview[ptype]
+		pic, found := p.Preview(ptype)
 		if !found {
 			return base
 		}
@@ -256,12 +255,12 @@ func (cf *CbddlpFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err e
 		doneMap[n] = make(chan layerInfo, cf.AntiAlias)
 	}
 
-	uv3dp.WithAllLayers(p, func(n int, layer uv3dp.Layer) {
+	uv3dp.WithAllLayers(p, func(p uv3dp.Printable, n int) {
 		for bit := 0; bit < cf.AntiAlias; bit++ {
-			rle, hash, bitsOn := rleEncodeBitmap(layer.Image, bit, cf.AntiAlias)
+			rle, hash, bitsOn := rleEncodeBitmap(p.LayerImage(n), bit, cf.AntiAlias)
 			doneMap[n] <- layerInfo{
-				Z:        layer.Z,
-				Exposure: layer.Exposure,
+				Z:        p.LayerZ(n),
+				Exposure: p.LayerExposure(n),
 				Rle:      rle,
 				Hash:     hash,
 				BitsOn:   bitsOn,
@@ -307,7 +306,7 @@ func (cf *CbddlpFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err e
 	header.LayerDefs = layerDefBase
 	header.LayerCount = uint32(size.Layers)
 	header.PreviewLow = previewTinyBase
-	header.PrintTime = uint32(properties.Duration())
+	header.PrintTime = uint32(uv3dp.PrintDuration(p) / time.Second)
 	header.Projector = 1 // LCD_X_MIRROR
 
 	if header.Version >= 2 {
@@ -409,7 +408,7 @@ func (cf *CbddlpFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err e
 	return
 }
 
-func (cf *CbddlpFormatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
+func (cf *Formatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
 	// Collect file
 	data, err := ioutil.ReadAll(file)
 	if err != nil {
@@ -563,10 +562,10 @@ func (cf *CbddlpFormatter) Decode(file uv3dp.Reader, filesize int64) (printable
 		exp.RetractHeight = defaultRetractHeight
 	}
 
-	cbd := &CbdDlp{
-		properties: prop,
-		layerDef:   layerDef,
-		rleMap:     rleMap,
+	cbd := &Print{
+		Print:    uv3dp.Print{Properties: prop},
+		layerDef: layerDef,
+		rleMap:   rleMap,
 	}
 
 	printable = cbd
@@ -574,36 +573,16 @@ func (cf *CbddlpFormatter) Decode(file uv3dp.Reader, filesize int64) (printable
 	return
 }
 
-// Properties get the properties of the CbdDlp Printable
-func (cbd *CbdDlp) Properties() (prop uv3dp.Properties) {
-	prop = cbd.properties
-
+// Layer gets a layer - we decode from the RLE on-the fly
+func (cbd *Print) LayerZ(index int) (z float32) {
+	z = cbd.layerDef[index].LayerHeight
 	return
 }
 
-// Layer gets a layer - we decode from the RLE on-the fly
-func (cbd *CbdDlp) Layer(index int) (layer uv3dp.Layer) {
-	if index < 0 || index >= len(cbd.layerDef) {
-		return
-	}
-
+func (cbd *Print) LayerExposure(index int) (exposure uv3dp.Exposure) {
 	layerDef := cbd.layerDef[index]
 
-	// Update per-layer info
-	prop := &cbd.properties
-	size := &prop.Size
-	bounds := image.Rect(0, 0, size.X, size.Y)
-	layerImage, err := rleDecodeBitmaps(bounds, cbd.rleMap[layerDef.ImageOffset])
-	if err != nil {
-		panic(err)
-	}
-
-	var exposure uv3dp.Exposure
-	if index < prop.Bottom.Count {
-		exposure = prop.Bottom.Exposure
-	} else {
-		exposure = prop.Exposure
-	}
+	exposure = cbd.Print.LayerExposure(index)
 
 	if layerDef.LayerExposure > 0.0 {
 		exposure.LightOnTime = layerDef.LayerExposure
@@ -613,10 +592,16 @@ func (cbd *CbdDlp) Layer(index int) (layer uv3dp.Layer) {
 		exposure.LightOffTime = layerDef.LayerOffTime
 	}
 
-	layer = uv3dp.Layer{
-		Z:        layerDef.LayerHeight,
-		Image:    layerImage,
-		Exposure: exposure,
+	return
+}
+
+func (cbd *Print) LayerImage(index int) (layerImage *image.Gray) {
+	layerDef := cbd.layerDef[index]
+
+	// Update per-layer info
+	layerImage, err := rleDecodeBitmaps(cbd.Bounds(), cbd.rleMap[layerDef.ImageOffset])
+	if err != nil {
+		panic(err)
 	}
 
 	return
diff --git a/cbddlp/init.go b/cbddlp/init.go
index bba79d8..7ca8fc7 100644
--- a/cbddlp/init.go
+++ b/cbddlp/init.go
@@ -23,7 +23,7 @@ var machines_cbddlp = map[string]uv3dp.Machine{
 }
 
 func init() {
-	newFormatter := func(suffix string) (format uv3dp.Formatter) { return NewCbddlpFormatter(suffix) }
+	newFormatter := func(suffix string) (format uv3dp.Formatter) { return NewFormatter(suffix) }
 
 	uv3dp.RegisterFormatter(".cbddlp", newFormatter)
 	uv3dp.RegisterFormatter(".photon", newFormatter)
diff --git a/cmd/uv3dp/bed.go b/cmd/uv3dp/bed.go
index c045f20..8929f9e 100644
--- a/cmd/uv3dp/bed.go
+++ b/cmd/uv3dp/bed.go
@@ -40,7 +40,7 @@ func NewBedCommand() (bc *BedCommand) {
 }
 
 func (bc *BedCommand) Filter(input uv3dp.Printable) (output uv3dp.Printable, err error) {
-	srcSize := input.Properties().Size
+	srcSize := input.Size()
 	dstSize := srcSize
 	rotate := false
 
@@ -153,26 +153,21 @@ type bedModifier struct {
 	reflect bool
 }
 
-func (bm *bedModifier) Properties() (prop uv3dp.Properties) {
-	prop = bm.Printable.Properties()
-
-	prop.Size = bm.size
+func (bm *bedModifier) Size() (size uv3dp.Size) {
+	size = bm.size
 
 	return
 }
 
-func (bm *bedModifier) Layer(index int) (layer uv3dp.Layer) {
-	layer = bm.Printable.Layer(index)
+func (bm *bedModifier) LayerImage(index int) (newImage *image.Gray) {
+	srcImage := image.Image(bm.Printable.LayerImage(index))
 
 	// Re-bed the layer to the new size
-	newImage := image.NewGray(image.Rect(0, 0, bm.size.X, bm.size.Y))
-
-	var srcImage image.Image
+	newImage = image.NewGray(image.Rect(0, 0, bm.size.X, bm.size.Y))
 
 	reflect := bm.reflect
 
 	// Our trivial rotation also causes a reflection, so invert the reflect operand
-	srcImage = layer.Image
 	if bm.rotate {
 		srcImage = &rotateImage{Image: srcImage}
 		reflect = !reflect
@@ -186,7 +181,5 @@ func (bm *bedModifier) Layer(index int) (layer uv3dp.Layer) {
 
 	draw.NearestNeighbor.Scale(newImage, bm.dstRect, srcImage, srcImage.Bounds(), draw.Src, nil)
 
-	layer.Image = newImage
-
 	return
 }
diff --git a/cmd/uv3dp/bottom.go b/cmd/uv3dp/bottom.go
index d39757b..eb177e0 100644
--- a/cmd/uv3dp/bottom.go
+++ b/cmd/uv3dp/bottom.go
@@ -41,32 +41,30 @@ func NewBottomCommand() (cmd *BottomCommand) {
 
 type bottomModifier struct {
 	uv3dp.Printable
-	Bottom uv3dp.Bottom
+	bottom uv3dp.Bottom
 }
 
-func (mod *bottomModifier) Properties() (prop uv3dp.Properties) {
-	prop = mod.Printable.Properties()
-
+func (mod *bottomModifier) Bottom() (bottom uv3dp.Bottom) {
 	// Set the bottom exposure
-	prop.Bottom = mod.Bottom
+	bottom = mod.bottom
 
 	return
 }
 
-func (mod *bottomModifier) Layer(index int) (layer uv3dp.Layer) {
-	layer = mod.Printable.Layer(index)
+func (mod *bottomModifier) LayerExposure(index int) (exposure uv3dp.Exposure) {
+	bot := mod.bottom
 
-	if index < mod.Bottom.Count {
-		layer.Exposure = mod.Bottom.Exposure
+	if index < bot.Count {
+		exposure = bot.Exposure
+	} else {
+		exposure = mod.Printable.LayerExposure(index)
 	}
 
 	return
 }
 
 func (cmd *BottomCommand) Filter(input uv3dp.Printable) (output uv3dp.Printable, err error) {
-	prop := input.Properties()
-
-	bot := prop.Bottom
+	bot := input.Bottom()
 
 	if cmd.Changed("count") {
 		TraceVerbosef(VerbosityNotice, "  Setting default bottom layer count %v", cmd.Count)
@@ -100,7 +98,7 @@ func (cmd *BottomCommand) Filter(input uv3dp.Printable) (output uv3dp.Printable,
 
 	mod := &bottomModifier{
 		Printable: input,
-		Bottom:    bot,
+		bottom:    bot,
 	}
 
 	output = mod
diff --git a/cmd/uv3dp/decimate.go b/cmd/uv3dp/decimate.go
index 8627eec..3c84a9f 100644
--- a/cmd/uv3dp/decimate.go
+++ b/cmd/uv3dp/decimate.go
@@ -33,8 +33,8 @@ func NewDecimateCommand() (cmd *DecimateCommand) {
 }
 
 func (cmd *DecimateCommand) Filter(input uv3dp.Printable) (output uv3dp.Printable, err error) {
-	layers := input.Properties().Size.Layers
-	botCount := input.Properties().Bottom.Count
+	layers := input.Size().Layers
+	botCount := input.Bottom().Count
 
 	if cmd.Bottom > 0 {
 		dec := uv3dp.NewDecimatedPrintable(input)
diff --git a/cmd/uv3dp/empty.go b/cmd/uv3dp/empty.go
index 12a5d2f..29cc5f5 100644
--- a/cmd/uv3dp/empty.go
+++ b/cmd/uv3dp/empty.go
@@ -56,7 +56,7 @@ func (ef *EmptyFormatter) Decode(file uv3dp.Reader, filesize int64) (printable u
 		size.Millimeter.Y = ef.Millimeters[1]
 	}
 
-	printable = uv3dp.NewEmptyPrintable(prop)
+	printable = &uv3dp.Print{Properties: prop}
 
 	return
 }
diff --git a/cmd/uv3dp/exposure.go b/cmd/uv3dp/exposure.go
index 1c1898f..375fe86 100644
--- a/cmd/uv3dp/exposure.go
+++ b/cmd/uv3dp/exposure.go
@@ -35,33 +35,31 @@ func NewExposureCommand() (cmd *ExposureCommand) {
 type exposureModifier struct {
 	uv3dp.Printable
 
-	Exposure    uv3dp.Exposure
-	BottomCount int
+	exposure uv3dp.Exposure
 }
 
-func (mod *exposureModifier) Properties() (prop uv3dp.Properties) {
-	prop = mod.Printable.Properties()
-
+func (mod *exposureModifier) Exposure() (exposure uv3dp.Exposure) {
 	// Set the normal exposure
-	prop.Exposure = mod.Exposure
+	exposure = mod.exposure
 
 	return
 }
 
-func (mod *exposureModifier) Layer(index int) (layer uv3dp.Layer) {
-	layer = mod.Printable.Layer(index)
+func (mod *exposureModifier) LayerExposure(index int) (exposure uv3dp.Exposure) {
+	exp := mod.exposure
+	bot := mod.Printable.Bottom()
 
-	if index >= mod.BottomCount {
-		layer.Exposure = mod.Exposure
+	if index < bot.Count {
+		exposure = mod.Printable.LayerExposure(index)
+	} else {
+		exposure = exp
 	}
 
 	return
 }
 
 func (cmd *ExposureCommand) Filter(input uv3dp.Printable) (mod uv3dp.Printable, err error) {
-	prop := input.Properties()
-
-	exp := prop.Exposure
+	exp := input.Exposure()
 
 	if cmd.Changed("light-on") {
 		TraceVerbosef(VerbosityNotice, "  Setting default exposure time to %v", cmd.LightOnTime)
@@ -79,9 +77,8 @@ func (cmd *ExposureCommand) Filter(input uv3dp.Printable) (mod uv3dp.Printable,
 	}
 
 	mod = &exposureModifier{
-		Printable:   input,
-		Exposure:    exp,
-		BottomCount: prop.Bottom.Count,
+		Printable: input,
+		exposure:  exp,
 	}
 
 	return
diff --git a/cmd/uv3dp/info.go b/cmd/uv3dp/info.go
index bbc33b5..dc05df2 100644
--- a/cmd/uv3dp/info.go
+++ b/cmd/uv3dp/info.go
@@ -7,6 +7,7 @@ package main
 import (
 	"fmt"
 	"sort"
+	"time"
 
 	"github.com/spf13/pflag"
 
@@ -37,18 +38,25 @@ func NewInfoCommand() (info *InfoCommand) {
 }
 
 func (info *InfoCommand) Filter(input uv3dp.Printable) (output uv3dp.Printable, err error) {
-	prop := input.Properties()
-
 	if info.SizeSummary {
-		size := &prop.Size
+		size := input.Size()
 		fmt.Printf("Layers: %v, %vx%v slices, %.2f x %.2f x %.2f mm bed required\n",
 			size.Layers, size.X, size.Y,
 			size.Millimeter.X, size.Millimeter.Y, float32(size.Layers)*size.LayerHeight)
+		exposureTime := time.Duration(0)
+		for n := 0; n < size.Layers; n++ {
+			exposureTime += time.Duration(input.LayerExposure(n).LightOnTime * float32(time.Second))
+		}
+		exposureTime = exposureTime.Truncate(time.Second)
+		totalTime := uv3dp.PrintDuration(input).Truncate(time.Second)
+
+		fmt.Printf("Total time: %v (%v exposure, %v motion)\n",
+			totalTime, exposureTime, totalTime-exposureTime)
 	}
 
 	if info.ExposureSummary {
-		exp := &prop.Exposure
-		bot := &prop.Bottom
+		exp := input.Exposure()
+		bot := input.Bottom()
 
 		fmt.Printf("Exposure: %.2gs on, %.2gs off",
 			exp.LightOnTime,
@@ -69,22 +77,22 @@ func (info *InfoCommand) Filter(input uv3dp.Printable) (output uv3dp.Printable,
 		fmt.Printf("Retract: %v mm, %v mm/min\n",
 			exp.RetractHeight, exp.RetractSpeed)
 
-		keys := []string{}
-		for k := range prop.Metadata {
-			keys = append(keys, k)
-		}
+		keys := input.MetadataKeys()
 
 		sort.Strings(keys)
 
 		for _, k := range keys {
-			fmt.Printf("%v: %v\n", k, prop.Metadata[k])
+			data, _ := input.Metadata(k)
+			fmt.Printf("%v: %v\n", k, data)
 		}
 	}
 
 	if info.LayerDetail {
-		for n := 0; n < prop.Size.Layers; n++ {
-			layer := input.Layer(n)
-			fmt.Printf("%d: @%.2f %+v\n", n, layer.Z, layer.Exposure)
+		size := input.Size()
+		for n := 0; n < size.Layers; n++ {
+			layerZ := input.LayerZ(n)
+			layerExposure := input.LayerExposure(n)
+			fmt.Printf("%d: @%.2f %+v\n", n, layerZ, layerExposure)
 		}
 	}
 
diff --git a/cmd/uv3dp/lift.go b/cmd/uv3dp/lift.go
index c82f6f1..0a4508b 100644
--- a/cmd/uv3dp/lift.go
+++ b/cmd/uv3dp/lift.go
@@ -32,33 +32,31 @@ func NewLiftCommand() (cmd *LiftCommand) {
 
 type liftModifier struct {
 	uv3dp.Printable
-	Exposure    uv3dp.Exposure
-	BottomCount int
+	exposure uv3dp.Exposure
 }
 
-func (mod *liftModifier) Properties() (prop uv3dp.Properties) {
-	prop = mod.Printable.Properties()
-
+func (mod *liftModifier) Exposure() (exposure uv3dp.Exposure) {
 	// Set the bottom and normal lift from the resins
-	prop.Exposure = mod.Exposure
+	exposure = mod.exposure
 
 	return
 }
 
-func (mod *liftModifier) Layer(index int) (layer uv3dp.Layer) {
-	layer = mod.Printable.Layer(index)
+func (mod *liftModifier) LayerExposure(index int) (exposure uv3dp.Exposure) {
+	exp := mod.exposure
+	bot := mod.Printable.Bottom()
 
-	if index >= mod.BottomCount {
-		layer.Exposure = mod.Exposure
+	if index < bot.Count {
+		exposure = mod.Printable.LayerExposure(index)
+	} else {
+		exposure = exp
 	}
 
 	return
 }
 
 func (cmd *LiftCommand) Filter(input uv3dp.Printable) (mod uv3dp.Printable, err error) {
-	prop := input.Properties()
-
-	exp := prop.Exposure
+	exp := input.Exposure()
 
 	if cmd.Changed("height") {
 		TraceVerbosef(VerbosityNotice, "  Setting default lift height to %v mm", cmd.LiftHeight)
@@ -71,9 +69,8 @@ func (cmd *LiftCommand) Filter(input uv3dp.Printable) (mod uv3dp.Printable, err
 	}
 
 	mod = &liftModifier{
-		Printable:   input,
-		Exposure:    exp,
-		BottomCount: prop.Bottom.Count,
+		Printable: input,
+		exposure:  exp,
 	}
 
 	return
diff --git a/cmd/uv3dp/resin.go b/cmd/uv3dp/resin.go
index 74dc427..c264ad1 100644
--- a/cmd/uv3dp/resin.go
+++ b/cmd/uv3dp/resin.go
@@ -35,39 +35,37 @@ type resinModifier struct {
 	Resin
 }
 
-func (mod *resinModifier) Properties() (prop uv3dp.Properties) {
-	prop = mod.Printable.Properties()
-
-	// Set the bottom and normal resin from the resins
-	prop.Bottom = mod.Resin.Bottom
-	prop.Exposure = mod.Resin.Exposure
+func (mod *resinModifier) Exposure() (exposure uv3dp.Exposure) {
+	exposure = mod.Resin.Exposure
 
 	return
 }
 
-func (mod *resinModifier) Layer(index int) (layer uv3dp.Layer) {
-	layer = mod.Printable.Layer(index)
+func (mod *resinModifier) Bottom() (bottom uv3dp.Bottom) {
+	bottom = mod.Resin.Bottom
 
+	return
+}
+
+func (mod *resinModifier) LayerExposure(index int) (exposure uv3dp.Exposure) {
 	exp := mod.Resin.Exposure
 	bot := mod.Resin.Bottom.Exposure
 	bottomCount := mod.Resin.Bottom.Count
 
 	if index < bottomCount {
-		layer.Exposure = bot
+		exposure = bot
 	} else {
-		layer.Exposure = exp
+		exposure = exp
 	}
 
 	return
 }
 
 func (cmd *ResinCommand) Filter(input uv3dp.Printable) (mod uv3dp.Printable, err error) {
-	prop := input.Properties()
-
 	// Clone the resin defaults from the source printable
 	resin := &Resin{
-		Exposure: prop.Exposure,
-		Bottom:   prop.Bottom,
+		Exposure: input.Exposure(),
+		Bottom:   input.Bottom(),
 	}
 
 	if cmd.Changed("type") {
diff --git a/cmd/uv3dp/retract.go b/cmd/uv3dp/retract.go
index ebfcad0..edcb342 100644
--- a/cmd/uv3dp/retract.go
+++ b/cmd/uv3dp/retract.go
@@ -32,33 +32,31 @@ func NewRetractCommand() (cmd *RetractCommand) {
 
 type retractModifier struct {
 	uv3dp.Printable
-	Exposure    uv3dp.Exposure
-	BottomCount int
+	exposure uv3dp.Exposure
 }
 
-func (mod *retractModifier) Properties() (prop uv3dp.Properties) {
-	prop = mod.Printable.Properties()
-
+func (mod *retractModifier) Exposure() (exposure uv3dp.Exposure) {
 	// Set the bottom and normal retract from the resins
-	prop.Exposure = mod.Exposure
+	exposure = mod.exposure
 
 	return
 }
 
-func (mod *retractModifier) Layer(index int) (layer uv3dp.Layer) {
-	layer = mod.Printable.Layer(index)
+func (mod *retractModifier) LayerExposure(index int) (exposure uv3dp.Exposure) {
+	bot := mod.Printable.Bottom()
+	exp := mod.exposure
 
-	if index >= mod.BottomCount {
-		layer.Exposure = mod.Exposure
+	if index < bot.Count {
+		exposure = mod.Printable.LayerExposure(index)
+	} else {
+		exposure = exp
 	}
 
 	return
 }
 
 func (cmd *RetractCommand) Filter(input uv3dp.Printable) (mod uv3dp.Printable, err error) {
-	prop := input.Properties()
-
-	exp := prop.Exposure
+	exp := input.Exposure()
 
 	if cmd.Changed("height") {
 		TraceVerbosef(VerbosityNotice, "  Setting default retract height to %v mm", cmd.RetractHeight)
@@ -71,9 +69,8 @@ func (cmd *RetractCommand) Filter(input uv3dp.Printable) (mod uv3dp.Printable, e
 	}
 
 	mod = &retractModifier{
-		Printable:   input,
-		Exposure:    exp,
-		BottomCount: prop.Bottom.Count,
+		Printable: input,
+		exposure:  exp,
 	}
 
 	return
diff --git a/cmd/uv3dp/select.go b/cmd/uv3dp/select.go
index 941559b..654f7ab 100644
--- a/cmd/uv3dp/select.go
+++ b/cmd/uv3dp/select.go
@@ -5,6 +5,8 @@
 package main
 
 import (
+	"image"
+
 	"github.com/spf13/pflag"
 
 	"github.com/ezrec/uv3dp"
@@ -40,21 +42,27 @@ type SelectPrintable struct {
 	count int
 }
 
-func (sp *SelectPrintable) Layer(index int) (layer uv3dp.Layer) {
-	layer = sp.Printable.Layer(index + sp.first)
+func (sp *SelectPrintable) LayerZ(index int) float32 {
+	return sp.Printable.LayerZ(index + sp.first)
+}
 
-	return
+func (sp *SelectPrintable) LayerExposure(index int) uv3dp.Exposure {
+	return sp.Printable.LayerExposure(index + sp.first)
+}
+
+func (sp *SelectPrintable) LayerImage(index int) *image.Gray {
+	return sp.Printable.LayerImage(index + sp.first)
 }
 
-func (sp *SelectPrintable) Properties() (prop uv3dp.Properties) {
-	prop = sp.Printable.Properties()
-	prop.Size.Layers = sp.count
+func (sp *SelectPrintable) Size() (size uv3dp.Size) {
+	size = sp.Printable.Size()
+	size.Layers = sp.count
 
 	return
 }
 
 func (cmd *SelectCommand) Filter(input uv3dp.Printable) (output uv3dp.Printable, err error) {
-	layers := input.Properties().Size.Layers
+	layers := input.Size().Layers
 
 	first := cmd.First
 	count := cmd.Count
diff --git a/ctb/format.go b/ctb/format.go
index 3d4e73c..e4f8c49 100644
--- a/ctb/format.go
+++ b/ctb/format.go
@@ -10,6 +10,7 @@ import (
 	"io/ioutil"
 	"math/rand"
 	"sort"
+	"time"
 
 	"encoding/binary"
 
@@ -138,26 +139,26 @@ type ctbImageInfo struct {
 	LightPWM     float32     // 50:
 }
 
-type Ctb struct {
-	properties uv3dp.Properties
-	layerDef   []ctbLayerDef
-	imageInfo  [](*ctbImageInfo)
+type Print struct {
+	uv3dp.Print
+	layerDef  []ctbLayerDef
+	imageInfo [](*ctbImageInfo)
 
 	rleMap map[uint32]([]byte)
 }
 
-type CtbFormatter struct {
+type Formatter struct {
 	*pflag.FlagSet
 
 	EncryptionSeed uint32
 	Version        int
 }
 
-func NewCtbFormatter(suffix string) (cf *CtbFormatter) {
+func NewFormatter(suffix string) (cf *Formatter) {
 	flagSet := pflag.NewFlagSet(suffix, pflag.ContinueOnError)
 	flagSet.SetInterspersed(false)
 
-	cf = &CtbFormatter{
+	cf = &Formatter{
 		FlagSet: flagSet,
 	}
 
@@ -168,17 +169,15 @@ func NewCtbFormatter(suffix string) (cf *CtbFormatter) {
 }
 
 // Save a uv3dp.Printable in CTB format
-func (cf *CtbFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err error) {
+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
 	}
 
-	properties := p.Properties()
-
-	size := &properties.Size
-	exp := &properties.Exposure
-	bot := &properties.Bottom
+	size := printable.Size()
+	exp := printable.Exposure()
+	bot := printable.Bottom()
 
 	// First, compute the rle images
 	type rleInfo struct {
@@ -211,7 +210,7 @@ func (cf *CtbFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err erro
 	rleHashList := []uint64{}
 
 	savePreview := func(base uint32, preview *ctbPreview, ptype uv3dp.PreviewType) uint32 {
-		pic, found := properties.Preview[ptype]
+		pic, found := printable.Preview(ptype)
 		if !found {
 			return base
 		}
@@ -278,11 +277,11 @@ func (cf *CtbFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err erro
 		doneMap[n] = make(chan layerInfo, 1)
 	}
 
-	uv3dp.WithAllLayers(p, func(n int, layer uv3dp.Layer) {
-		rle, hash, bitsOn := rleEncodeGraymap(layer.Image)
+	uv3dp.WithAllLayers(printable, func(p uv3dp.Printable, n int) {
+		rle, hash, bitsOn := rleEncodeGraymap(p.LayerImage(n))
 		doneMap[n] <- layerInfo{
-			Z:        layer.Z,
-			Exposure: layer.Exposure,
+			Z:        p.LayerZ(n),
+			Exposure: p.LayerExposure(n),
 			Rle:      rle,
 			Hash:     hash,
 			BitsOn:   bitsOn,
@@ -348,7 +347,7 @@ func (cf *CtbFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err erro
 	header.LayerDefs = layerDefBase
 	header.LayerCount = uint32(size.Layers)
 	header.PreviewLow = previewTinyBase
-	header.PrintTime = uint32(properties.Duration())
+	header.PrintTime = uint32(uv3dp.PrintDuration(printable) / time.Second)
 	header.Projector = 1 // LCD_X_MIRROR
 
 	header.ParamOffset = paramBase
@@ -487,7 +486,7 @@ func cipher(seed uint32, slice uint32, in []byte) (out []byte) {
 	return
 }
 
-func (cf *CtbFormatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
+func (cf *Formatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
 	// Collect file
 	data, err := ioutil.ReadAll(file)
 	if err != nil {
@@ -638,47 +637,37 @@ func (cf *CtbFormatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3
 		exp.RetractHeight = defaultRetractHeight
 	}
 
-	cbd := &Ctb{
-		properties: prop,
-		layerDef:   layerDef,
-		imageInfo:  imageInfo,
-		rleMap:     rleMap,
+	ctb := &Print{
+		Print:     uv3dp.Print{Properties: prop},
+		layerDef:  layerDef,
+		imageInfo: imageInfo,
+		rleMap:    rleMap,
 	}
 
-	printable = cbd
-
-	return
-}
-
-// Properties get the properties of the Ctb Printable
-func (cbd *Ctb) Properties() (prop uv3dp.Properties) {
-	prop = cbd.properties
+	printable = ctb
 
 	return
 }
 
-// Layer gets a layer - we decode from the RLE on-the fly
-func (cbd *Ctb) Layer(index int) (layer uv3dp.Layer) {
-	if index < 0 || index >= len(cbd.layerDef) {
-		return
-	}
-
-	layerDef := cbd.layerDef[index]
+func (ctb *Print) LayerImage(index int) (layerImage *image.Gray) {
+	layerDef := ctb.layerDef[index]
 
 	// Update per-layer info
-	prop := &cbd.properties
-	size := &prop.Size
-	bounds := image.Rect(0, 0, size.X, size.Y)
-	layerImage, err := rleDecodeGraymap(bounds, cbd.rleMap[layerDef.ImageOffset])
+	layerImage, err := rleDecodeGraymap(ctb.Bounds(), ctb.rleMap[layerDef.ImageOffset])
 	if err != nil {
 		panic(err)
 	}
 
-	var exposure uv3dp.Exposure
-	if index < prop.Bottom.Count {
-		exposure = prop.Bottom.Exposure
+	return
+}
+
+func (ctb *Print) LayerExposure(index int) (exposure uv3dp.Exposure) {
+	layerDef := ctb.layerDef[index]
+
+	if index < ctb.Bottom().Count {
+		exposure = ctb.Bottom().Exposure
 	} else {
-		exposure = prop.Exposure
+		exposure = ctb.Exposure()
 	}
 
 	if layerDef.LayerExposure > 0.0 {
@@ -690,7 +679,7 @@ func (cbd *Ctb) Layer(index int) (layer uv3dp.Layer) {
 	}
 
 	// See if we have per-layer overrides
-	info := cbd.imageInfo[index]
+	info := ctb.imageInfo[index]
 	if info != nil {
 		exposure.LightOnTime = info.LayerDef.LayerExposure
 		exposure.LightOffTime = info.LayerDef.LayerOffTime
@@ -700,11 +689,10 @@ func (cbd *Ctb) Layer(index int) (layer uv3dp.Layer) {
 		exposure.RetractSpeed = info.RetractSpeed
 	}
 
-	layer = uv3dp.Layer{
-		Z:        layerDef.LayerHeight,
-		Image:    layerImage,
-		Exposure: exposure,
-	}
+	return
+}
 
+func (ctb *Print) LayerZ(index int) (z float32) {
+	z = ctb.layerDef[index].LayerHeight
 	return
 }
diff --git a/ctb/init.go b/ctb/init.go
index f9d6624..a9269c8 100644
--- a/ctb/init.go
+++ b/ctb/init.go
@@ -23,7 +23,7 @@ var (
 )
 
 func init() {
-	newFormatter := func(suffix string) (format uv3dp.Formatter) { return NewCtbFormatter(suffix) }
+	newFormatter := func(suffix string) (format uv3dp.Formatter) { return NewFormatter(suffix) }
 
 	uv3dp.RegisterFormatter(".ctb", newFormatter)
 
diff --git a/cws/format.go b/cws/format.go
index f2ff921..554714e 100644
--- a/cws/format.go
+++ b/cws/format.go
@@ -249,20 +249,20 @@ type ReadSeekCloser interface {
 	io.Closer
 }
 
-type CWS struct {
-	properties uv3dp.Properties
-	config     cwsConfig
-	layerPng   []([]byte)
+type Print struct {
+	uv3dp.Print
+	config   cwsConfig
+	layerPng []([]byte)
 }
 
-type CWSFormat struct {
+type Format struct {
 	*pflag.FlagSet
 }
 
-func NewCWSFormatter(suffix string) (sf *CWSFormat) {
+func NewFormatter(suffix string) (sf *Format) {
 	flagSet := pflag.NewFlagSet(suffix, pflag.ContinueOnError)
 
-	sf = &CWSFormat{
+	sf = &Format{
 		FlagSet: flagSet,
 	}
 
@@ -271,17 +271,15 @@ func NewCWSFormatter(suffix string) (sf *CWSFormat) {
 	return
 }
 
-func (sf *CWSFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err error) {
+func (sf *Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err error) {
 	jobName := defaultName
 
 	archive := zip.NewWriter(writer)
 	defer archive.Close()
 
-	prop := printable.Properties()
-
-	size := &prop.Size
-	exp := &prop.Exposure
-	bot := &prop.Bottom.Exposure
+	size := printable.Size()
+	exp := printable.Exposure()
+	bot := printable.Bottom().Exposure
 
 	if exp.LightPWM == 0 {
 		exp.LightPWM = 255
@@ -291,7 +289,7 @@ func (sf *CWSFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 		bot.LightPWM = 255
 	}
 
-	uv3dp.WithEachLayer(printable, func(n int, layer uv3dp.Layer) {
+	uv3dp.WithEachLayer(printable, func(p uv3dp.Printable, n int) {
 		filename := fmt.Sprintf("%s%04d.png", jobName, n)
 
 		var writer io.Writer
@@ -300,7 +298,7 @@ func (sf *CWSFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 			return
 		}
 
-		err = png.Encode(writer, layer.Image)
+		err = png.Encode(writer, p.LayerImage(n))
 		if err != nil {
 			return
 		}
@@ -322,7 +320,7 @@ func (sf *CWSFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 		LayerTime:           int(exp.LightOnTime * 1000.0),
 		OutlineWidthInset:   2,
 		BottomLayersTime:    int(bot.LightOnTime * 1000.0),
-		BottomLayers:        prop.Bottom.Count,
+		BottomLayers:        printable.Bottom().Count,
 		BlankingLayerTime:   int(exp.LightOffTime * 1000.0),
 		BuildDirection:      "Bottom_Up",
 		LiftDistance:        int(exp.LiftHeight),
@@ -361,20 +359,21 @@ M106 S0
 	// Create all the layer movement gcode
 	priorZ := float32(0.0)
 	for n := 0; n < size.Layers; n++ {
-		layer := printable.Layer(n)
+		layerZ := printable.LayerZ(n)
+		layerExposure := printable.LayerExposure(n)
 		if n > 0 {
-			thickness := layer.Z - priorZ
-			fmt.Fprintf(gcode, "G1 Z%1.3f F%v\n", -(layer.Exposure.LiftHeight - thickness), int(layer.Exposure.LiftSpeed))
+			thickness := layerZ - priorZ
+			fmt.Fprintf(gcode, "G1 Z%1.3f F%v\n", -(layerExposure.LiftHeight - thickness), int(layerExposure.LiftSpeed))
 			// This is just a guess here
-			fmt.Fprintf(gcode, ";<Delay> %v\n", 720000/int(layer.Exposure.LiftSpeed))
+			fmt.Fprintf(gcode, ";<Delay> %v\n", 720000/int(layerExposure.LiftSpeed))
 		}
 
 		// Create all the layers
 		fmt.Fprintf(gcode, "\n;<Slice> %v\n", n)
-		fmt.Fprintf(gcode, "M106 S%v\n;<Delay> %v\n", layer.Exposure.LightPWM, int(layer.Exposure.LightOnTime*1000.0))
+		fmt.Fprintf(gcode, "M106 S%v\n;<Delay> %v\n", layerExposure.LightPWM, int(layerExposure.LightOnTime*1000.0))
 		fmt.Fprintf(gcode, "M106 S0\n;<Slice> Blank\n")
-		fmt.Fprintf(gcode, "G1 Z%1.3f F%v\n", layer.Exposure.LiftHeight, int(layer.Exposure.LiftSpeed))
-		priorZ = layer.Z
+		fmt.Fprintf(gcode, "G1 Z%1.3f F%v\n", layerExposure.LiftHeight, int(layerExposure.LiftSpeed))
+		priorZ = layerZ
 	}
 
 	// Emit the GCode trailer
@@ -387,7 +386,7 @@ M106 S0
 	return
 }
 
-func (sf *CWSFormat) Decode(reader uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
+func (sf *Format) Decode(reader uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
 	archive, err := zip.NewReader(reader, filesize)
 	if err != nil {
 		return
@@ -503,9 +502,9 @@ func (sf *CWSFormat) Decode(reader uv3dp.Reader, filesize int64) (printable uv3d
 	bot.LiftSpeed = exp.LiftSpeed
 	bot.RetractSpeed = exp.RetractSpeed
 
-	cws := &CWS{
-		properties: prop,
-		layerPng:   layerPng,
+	cws := &Print{
+		Print:    uv3dp.Print{Properties: prop},
+		layerPng: layerPng,
 	}
 
 	printable = cws
@@ -513,27 +512,15 @@ func (sf *CWSFormat) Decode(reader uv3dp.Reader, filesize int64) (printable uv3d
 	return
 }
 
-func (cws *CWS) Close() {
+func (cws *Print) Close() {
 }
 
-func (cws *CWS) Properties() (prop uv3dp.Properties) {
-	prop = cws.properties
-	return
-}
-
-func (cws *CWS) Layer(index int) (layer uv3dp.Layer) {
+func (cws *Print) LayerImage(index int) (imageGray *image.Gray) {
 	pngImage, err := png.Decode(bytes.NewReader(cws.layerPng[index]))
 	if err != nil {
 		panic(err)
 	}
-
-	layer.Z = cws.properties.LayerZ(index)
-	layer.Image = pngImage.(*image.Gray)
-	layer.Exposure = cws.properties.LayerExposure(index)
-
-	if layer.Exposure.LightPWM == 0 {
-		layer.Exposure.LightPWM = 255
-	}
+	imageGray = pngImage.(*image.Gray)
 
 	return
 }
diff --git a/cws/init.go b/cws/init.go
index f287092..09a0181 100644
--- a/cws/init.go
+++ b/cws/init.go
@@ -16,7 +16,7 @@ var (
 )
 
 func init() {
-	newFormatter := func(suffix string) uv3dp.Formatter { return NewCWSFormatter(suffix) }
+	newFormatter := func(suffix string) uv3dp.Formatter { return NewFormatter(suffix) }
 
 	uv3dp.RegisterFormatter(".cws", newFormatter)
 
diff --git a/decimate.go b/decimate.go
index afea6c0..c7c1f5f 100644
--- a/decimate.go
+++ b/decimate.go
@@ -20,18 +20,18 @@ func NewDecimatedPrintable(printable Printable) (dp *DecimatedPrintable) {
 		Printable:  printable,
 		Passes:     1,
 		FirstLayer: 0,
-		Layers:     printable.Properties().Size.Layers,
+		Layers:     printable.Size().Layers,
 	}
 
 	return
 }
 
-func (dec *DecimatedPrintable) Layer(index int) (layer Layer) {
-	layer = dec.Printable.Layer(index)
+func (dec *DecimatedPrintable) LayerImage(index int) (ig *image.Gray) {
+	ig = dec.Printable.LayerImage(index)
 
 	if index >= dec.FirstLayer && ((index - dec.FirstLayer) < dec.Layers) {
 		for pass := 0; pass < dec.Passes; pass++ {
-			layer.Image = decimateGray(layer.Image)
+			ig = decimateGray(ig)
 		}
 	}
 
diff --git a/empty.go b/empty.go
deleted file mode 100644
index 57faa35..0000000
--- a/empty.go
+++ /dev/null
@@ -1,42 +0,0 @@
-//
-// Copyright (c) 2020 Jason S. McMullan <jason.mcmullan@gmail.com>
-//
-
-package uv3dp
-
-import (
-	"image"
-)
-
-type EmptyPrintable struct {
-	properties Properties
-}
-
-func NewEmptyPrintable(prop Properties) (empty *EmptyPrintable) {
-	empty = &EmptyPrintable{
-		properties: prop,
-	}
-
-	return
-}
-
-func (empty *EmptyPrintable) Properties() (prop Properties) {
-	prop = empty.properties
-
-	return
-}
-
-func (empty *EmptyPrintable) Layer(index int) (layer Layer) {
-	prop := empty.Properties()
-
-	layer.Z = prop.LayerZ(index)
-	layer.Image = image.NewGray(prop.Bounds())
-
-	if index < prop.Bottom.Count {
-		layer.Exposure = prop.Bottom.Exposure
-	} else {
-		layer.Exposure = prop.Exposure
-	}
-
-	return
-}
diff --git a/phz/format.go b/phz/format.go
index 7c04f34..d3254fd 100644
--- a/phz/format.go
+++ b/phz/format.go
@@ -9,6 +9,7 @@ import (
 	"image"
 	"io/ioutil"
 	"sort"
+	"time"
 
 	"encoding/binary"
 
@@ -98,24 +99,24 @@ type phzLayerDef struct {
 	_             [4]uint32 // 14:
 }
 
-type Phz struct {
-	properties uv3dp.Properties
-	layerDef   []phzLayerDef
+type Print struct {
+	uv3dp.Print
+	layerDef []phzLayerDef
 
 	rleMap map[uint32]([]byte)
 }
 
-type PhzFormatter struct {
+type Formatter struct {
 	*pflag.FlagSet
 
 	EncryptionSeed uint32
 }
 
-func NewPhzFormatter(suffix string) (pf *PhzFormatter) {
+func NewFormatter(suffix string) (pf *Formatter) {
 	flagSet := pflag.NewFlagSet(suffix, pflag.ContinueOnError)
 	flagSet.SetInterspersed(false)
 
-	pf = &PhzFormatter{
+	pf = &Formatter{
 		FlagSet: flagSet,
 	}
 
@@ -125,12 +126,10 @@ func NewPhzFormatter(suffix string) (pf *PhzFormatter) {
 }
 
 // Save a uv3dp.Printable in CBD DLP format
-func (pf *PhzFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err error) {
-	properties := p.Properties()
-
-	size := &properties.Size
-	exp := &properties.Exposure
-	bot := &properties.Bottom
+func (pf *Formatter) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err error) {
+	size := printable.Size()
+	exp := printable.Exposure()
+	bot := printable.Bottom()
 
 	// First, compute the rle images
 	type rleInfo struct {
@@ -160,7 +159,7 @@ func (pf *PhzFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err erro
 	rleHashList := []uint64{}
 
 	savePreview := func(base uint32, preview *phzPreview, ptype uv3dp.PreviewType) uint32 {
-		pic, found := properties.Preview[ptype]
+		pic, found := printable.Preview(ptype)
 		if !found {
 			return base
 		}
@@ -218,11 +217,11 @@ func (pf *PhzFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err erro
 		doneMap[n] = make(chan layerInfo, 1)
 	}
 
-	uv3dp.WithAllLayers(p, func(n int, layer uv3dp.Layer) {
-		rle, hash, bitsOn := rleEncodeGraymap(layer.Image)
+	uv3dp.WithAllLayers(printable, func(p uv3dp.Printable, n int) {
+		rle, hash, bitsOn := rleEncodeGraymap(p.LayerImage(n))
 		doneMap[n] <- layerInfo{
-			Z:        layer.Z,
-			Exposure: layer.Exposure,
+			Z:        p.LayerZ(n),
+			Exposure: p.LayerExposure(n),
 			Rle:      rle,
 			Hash:     hash,
 			BitsOn:   bitsOn,
@@ -270,7 +269,7 @@ func (pf *PhzFormatter) Encode(writer uv3dp.Writer, p uv3dp.Printable) (err erro
 	header.LayerDefs = layerDefBase
 	header.LayerCount = uint32(size.Layers)
 	header.PreviewLow = previewTinyBase
-	header.PrintTime = uint32(properties.Duration())
+	header.PrintTime = uint32(uv3dp.PrintDuration(printable) / time.Second)
 	header.Projector = 1 // LCD_X_MIRROR
 
 	header.AntiAliasLevel = 1
@@ -386,7 +385,7 @@ func cipher(seed uint32, slice uint32, in []byte) (out []byte) {
 	return
 }
 
-func (pf *PhzFormatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
+func (pf *Formatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
 	// Collect file
 	data, err := ioutil.ReadAll(file)
 	if err != nil {
@@ -494,10 +493,10 @@ func (pf *PhzFormatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3
 	exp.RetractSpeed = header.RetractSpeed
 	exp.RetractHeight = defaultRetractHeight
 
-	phz := &Phz{
-		properties: prop,
-		layerDef:   layerDef,
-		rleMap:     rleMap,
+	phz := &Print{
+		Print:    uv3dp.Print{Properties: prop},
+		layerDef: layerDef,
+		rleMap:   rleMap,
 	}
 
 	printable = phz
@@ -505,36 +504,23 @@ func (pf *PhzFormatter) Decode(file uv3dp.Reader, filesize int64) (printable uv3
 	return
 }
 
-// Properties get the properties of the Phz Printable
-func (phz *Phz) Properties() (prop uv3dp.Properties) {
-	prop = phz.properties
-
-	return
-}
-
 // Layer gets a layer - we decode from the RLE on-the fly
-func (phz *Phz) Layer(index int) (layer uv3dp.Layer) {
-	if index < 0 || index >= len(phz.layerDef) {
-		return
-	}
-
+func (phz *Print) LayerImage(index int) (layerImage *image.Gray) {
 	layerDef := phz.layerDef[index]
 
 	// Update per-layer info
-	prop := &phz.properties
-	size := &prop.Size
-	bounds := image.Rect(0, 0, size.X, size.Y)
-	layerImage, err := rleDecodeGraymap(bounds, phz.rleMap[layerDef.ImageOffset])
+	layerImage, err := rleDecodeGraymap(phz.Bounds(), phz.rleMap[layerDef.ImageOffset])
 	if err != nil {
 		panic(err)
 	}
 
-	var exposure uv3dp.Exposure
-	if index < prop.Bottom.Count {
-		exposure = prop.Bottom.Exposure
-	} else {
-		exposure = prop.Exposure
-	}
+	return
+}
+
+func (phz *Print) LayerExposure(index int) (exposure uv3dp.Exposure) {
+	layerDef := phz.layerDef[index]
+
+	exposure = phz.Print.LayerExposure(index)
 
 	if layerDef.LayerExposure > 0.0 {
 		exposure.LightOnTime = layerDef.LayerExposure
@@ -544,11 +530,10 @@ func (phz *Phz) Layer(index int) (layer uv3dp.Layer) {
 		exposure.LightOffTime = layerDef.LayerOffTime
 	}
 
-	layer = uv3dp.Layer{
-		Z:        layerDef.LayerHeight,
-		Image:    layerImage,
-		Exposure: exposure,
-	}
+	return
+}
 
+func (phz *Print) LayerZ(index int) (z float32) {
+	z = phz.layerDef[index].LayerHeight
 	return
 }
diff --git a/phz/init.go b/phz/init.go
index 17cfb54..728777d 100644
--- a/phz/init.go
+++ b/phz/init.go
@@ -16,7 +16,7 @@ var (
 )
 
 func init() {
-	newFormatter := func(suffix string) (format uv3dp.Formatter) { return NewPhzFormatter(suffix) }
+	newFormatter := func(suffix string) (format uv3dp.Formatter) { return NewFormatter(suffix) }
 
 	uv3dp.RegisterFormatter(".phz", newFormatter)
 
diff --git a/print.go b/print.go
new file mode 100644
index 0000000..b7cb796
--- /dev/null
+++ b/print.go
@@ -0,0 +1,47 @@
+//
+// Copyright (c) 2020 Jason S. McMullan <jason.mcmullan@gmail.com>
+//
+
+package uv3dp
+
+import (
+	"image"
+)
+
+type Print struct {
+	Properties
+}
+
+func (p *Print) Size() Size {
+	return p.Properties.Size
+}
+
+func (p *Print) Exposure() Exposure {
+	return p.Properties.Exposure
+}
+
+func (p *Print) Bottom() Bottom {
+	return p.Properties.Bottom
+}
+
+func (p *Print) Preview(index PreviewType) (ig image.Image, ok bool) {
+	ig, ok = p.Properties.Preview[index]
+	return
+}
+
+func (p *Print) Metadata(index string) (data interface{}, ok bool) {
+	data, ok = p.Properties.Metadata[index]
+	return
+}
+
+func (p *Print) LayerImage(index int) (ig *image.Gray) {
+	return image.NewGray(p.Properties.Bounds())
+}
+
+func NewEmptyPrintable(prop Properties) (p *Print) {
+	p = &Print{
+		Properties: prop,
+	}
+
+	return
+}
diff --git a/printable.go b/printable.go
index 7a1f3cb..d9ec4da 100644
--- a/printable.go
+++ b/printable.go
@@ -8,25 +8,24 @@ import (
 	"image"
 	"runtime"
 	"sync"
+	"time"
 )
 
-// Everything needed to print a single layer
-type Layer struct {
-	Z        float32     // Z height in mm
-	Exposure Exposure    // Layer exposure settings
-	Image    *image.Gray `json:",omitempty"` // Image mask
-}
-
 type Printable interface {
-	Properties() (prop Properties)
-	Layer(index int) (layer Layer)
+	Size() Size
+	Exposure() Exposure
+	Bottom() Bottom
+	Preview(index PreviewType) (image.Image, bool)
+	MetadataKeys() []string
+	Metadata(key string) (data interface{}, ok bool)
+	LayerZ(index int) float32
+	LayerExposure(index int) Exposure
+	LayerImage(index int) *image.Gray
 }
 
 // WithAllLayers executes a function in parallel over all of the layers
-func WithAllLayers(p Printable, do func(n int, layer Layer)) {
-	prop := p.Properties()
-
-	layers := prop.Size.Layers
+func WithAllLayers(p Printable, do func(p Printable, n int)) {
+	layers := p.Size().Layers
 
 	prog := NewProgress(layers)
 	defer prog.Close()
@@ -34,11 +33,9 @@ func WithAllLayers(p Printable, do func(n int, layer Layer)) {
 	guard := make(chan struct{}, runtime.GOMAXPROCS(0))
 	for n := 0; n < layers; n++ {
 		guard <- struct{}{}
-		go func(p Printable, do func(n int, layer Layer), n int) {
-			layer := p.Layer(n)
-			do(n, layer)
+		go func(p Printable, do func(p Printable, n int), n int) {
+			do(p, n)
 			prog.Indicate()
-			layer.Image = nil
 			runtime.GC()
 			<-guard
 		}(p, do, n)
@@ -46,12 +43,24 @@ func WithAllLayers(p Printable, do func(n int, layer Layer)) {
 }
 
 // WithEachLayer executes a function in over all of the layers, serially (but possibly out of order)
-func WithEachLayer(p Printable, do func(n int, layer Layer)) {
+func WithEachLayer(p Printable, do func(p Printable, n int)) {
 	var mutex sync.Mutex
 
-	WithAllLayers(p, func(n int, layer Layer) {
+	WithAllLayers(p, func(p Printable, n int) {
 		mutex.Lock()
-		do(n, layer)
+		do(p, n)
 		mutex.Unlock()
 	})
 }
+
+// Get the total print time for a printable
+func PrintDuration(p Printable) (duration time.Duration) {
+	layers := p.Size().Layers
+
+	for n := 0; n < layers; n++ {
+		exposure := p.LayerExposure(n)
+		duration += exposure.Duration()
+	}
+
+	return
+}
diff --git a/properties.go b/properties.go
index c42f961..069ff1a 100644
--- a/properties.go
+++ b/properties.go
@@ -7,6 +7,7 @@ package uv3dp
 import (
 	"image"
 	"math"
+	"time"
 )
 
 type SizeMillimeter struct {
@@ -32,12 +33,15 @@ type Exposure struct {
 }
 
 // Total duration of an exposure
-func (exp *Exposure) Duration() (total float32) {
-	total = exp.LightOnTime + exp.LightOffTime
+func (exp *Exposure) Duration() (total time.Duration) {
+	totalSec := exp.LightOnTime + exp.LightOffTime
 
 	// Motion is lift; then retract -> move back to start at retract speed
-	total += exp.LiftHeight / exp.LiftSpeed
-	total += (exp.LiftHeight + exp.RetractHeight*2) / exp.RetractSpeed
+	totalSec += exp.LiftHeight / exp.LiftSpeed * 60
+	totalSec += (exp.LiftHeight + exp.RetractHeight*2) / exp.RetractSpeed * 60
+
+	total = time.Duration(totalSec * float32(time.Second))
+
 	return
 }
 
@@ -89,38 +93,36 @@ func (prop *Properties) GetMetadataUint8(attr string, defValue uint8) (value uin
 	return
 }
 
+func (prop *Properties) MetadataKeys() (keys []string) {
+	for key := range prop.Metadata {
+		keys = append(keys, key)
+	}
+
+	return
+}
+
 // Get image bounds
 func (prop *Properties) Bounds() image.Rectangle {
 	return image.Rect(0, 0, prop.Size.X, prop.Size.Y)
 }
 
-// Duration returns total printing time
-func (prop *Properties) Duration() (duration float32) {
-	size := &prop.Size
-	bot := &prop.Bottom.Exposure
-	botCount := prop.Bottom.Count
-	exp := &prop.Exposure
-	botTime := bot.Duration() * float32(botCount)
-	expTime := exp.Duration() * float32(size.Layers-int(botCount))
-
-	duration = botTime + expTime
-
-	return
-}
-
-// LayerExposure gets the default exposure by layer index
+// Exposure gets the default exposure by layer index
 func (prop *Properties) LayerExposure(index int) (exposure Exposure) {
 	if index >= prop.Bottom.Count {
 		exposure = prop.Exposure
-		return
+	} else {
+		exposure = prop.Bottom.Exposure
 	}
 
-	exposure = prop.Bottom.Exposure
+	// Validate LightPWM
+	if exposure.LightPWM == 0 {
+		exposure.LightPWM = 255
+	}
 
 	return
 }
 
-// LayerZ get the default Z height at a layer index
+// Z get the default Z height at a layer index
 func (prop *Properties) LayerZ(index int) (z float32) {
 	return float32(math.Round(float64(prop.Size.LayerHeight)*float64(index+1)*100) / 100.0)
 }
diff --git a/pws/format.go b/pws/format.go
index e966714..f60d47d 100644
--- a/pws/format.go
+++ b/pws/format.go
@@ -283,7 +283,7 @@ func (layerdef *LayerDef) Unmarshal(data []byte) (err error) {
 }
 
 type Print struct {
-	properties       uv3dp.Properties
+	uv3dp.Print
 	perLayerOverride bool
 	layers           []Layer
 }
@@ -317,7 +317,9 @@ func NewFormatter(suffix string) (sf *Format) {
 }
 
 func (sf *Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err error) {
-	prop := printable.Properties()
+	size := printable.Size()
+	exposure := printable.Exposure()
+	bottom := printable.Bottom()
 
 	filemark := Filemark{
 		Mark:    sectionMarkFilemark,
@@ -327,41 +329,42 @@ func (sf *Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err er
 
 	header := Header{
 		// TODO: Check for 'squareness' of pixels?
-		PixelSize:         prop.Size.Millimeter.X / float32(prop.Size.X) * 1000.0,
-		LayerHeight:       prop.Size.LayerHeight,
-		LightOnTime:       prop.Exposure.LightOnTime,
-		LightOffTime:      prop.Exposure.LightOffTime,
-		BottomLightOnTime: prop.Bottom.Exposure.LightOnTime,
-		BottomLayers:      float32(prop.Bottom.Count),
-		LiftHeight:        prop.Exposure.LiftHeight,
-		LiftSpeed:         prop.Exposure.LiftSpeed / 60.0,
-		RetractSpeed:      prop.Exposure.RetractSpeed / 60.0,
+		PixelSize:         size.Millimeter.X / float32(size.X) * 1000.0,
+		LayerHeight:       size.LayerHeight,
+		LightOnTime:       exposure.LightOnTime,
+		LightOffTime:      exposure.LightOffTime,
+		BottomLightOnTime: bottom.Exposure.LightOnTime,
+		BottomLayers:      float32(bottom.Count),
+		LiftHeight:        exposure.LiftHeight,
+		LiftSpeed:         exposure.LiftSpeed / 60.0,
+		RetractSpeed:      exposure.RetractSpeed / 60.0,
 		AntiAlias:         uint32(sf.AntiAlias),
-		ResolutionX:       uint32(prop.Size.X),
-		ResolutionY:       uint32(prop.Size.Y),
+		ResolutionX:       uint32(size.X),
+		ResolutionY:       uint32(size.Y),
 		PerLayerOverride:  1, // true
 	}
 
 	var preview Preview
 
-	previewImage, ok := prop.Preview[uv3dp.PreviewTypeTiny]
+	previewImage, ok := printable.Preview(uv3dp.PreviewTypeTiny)
 	if ok {
 		preview.SetImage(previewImage)
 	}
 
-	layers := make([]Layer, prop.Size.Layers)
+	layers := make([]Layer, size.Layers)
 
-	uv3dp.WithAllLayers(printable, func(n int, layer uv3dp.Layer) {
+	uv3dp.WithAllLayers(printable, func(p uv3dp.Printable, n int) {
+		exposure := p.LayerExposure(n)
 		l := Layer{
-			LiftHeight:  layer.Exposure.LiftHeight,
-			LiftSpeed:   layer.Exposure.LiftSpeed,
-			LightOnTime: layer.Exposure.LightOnTime,
+			LiftHeight:  exposure.LiftHeight,
+			LiftSpeed:   exposure.LiftSpeed,
+			LightOnTime: exposure.LightOnTime,
 			LayerHeight: header.LayerHeight,
 		}
 
 		l.slice.AntiAlias = sf.AntiAlias
 		l.slice.Format = sf.sliceFormat
-		l.slice.SetImage(layer.Image)
+		l.slice.SetImage(p.LayerImage(n))
 
 		layers[n] = l
 	})
@@ -556,7 +559,7 @@ func (sf *Format) Decode(reader uv3dp.Reader, filesize int64) (printable uv3dp.P
 	}
 
 	printable = &Print{
-		properties:       prop,
+		Print:            uv3dp.Print{Properties: prop},
 		layers:           layerdef.Layer,
 		perLayerOverride: header.PerLayerOverride != 0,
 	}
@@ -567,29 +570,25 @@ func (sf *Format) Decode(reader uv3dp.Reader, filesize int64) (printable uv3dp.P
 func (sf *Print) Close() {
 }
 
-func (pws *Print) Properties() (prop uv3dp.Properties) {
-	prop = pws.properties
-	return
-}
-
-func (pws *Print) Layer(index int) (layer uv3dp.Layer) {
+func (pws *Print) LayerExposure(index int) (exposure uv3dp.Exposure) {
 	l := pws.layers[index]
 
-	prop := &pws.properties
-	layer.Exposure = prop.LayerExposure(index)
+	exposure = pws.Print.LayerExposure(index)
 
 	if pws.perLayerOverride {
-		layer.Exposure.LightOnTime = l.LightOnTime
-		layer.Exposure.LiftHeight = l.LiftHeight
-		layer.Exposure.LiftSpeed = l.LiftSpeed * 60
+		exposure.LightOnTime = l.LightOnTime
+		exposure.LiftHeight = l.LiftHeight
+		exposure.LiftSpeed = l.LiftSpeed * 60
 	}
 
+	return
+}
+
+func (pws *Print) Image(index int) (slice *image.Gray) {
 	slice, err := pws.layers[index].slice.GetImage()
 	if err != nil {
-		panic(fmt.Sprintf("pws: layer %v/%v: %s", index+1, prop.Size.Layers, err))
+		panic(fmt.Sprintf("pws: layer %v: %s", index+1, err))
 	}
 
-	layer.Z = prop.LayerZ(index)
-	layer.Image = slice
 	return
 }
diff --git a/sl1/format.go b/sl1/format.go
index 648a56f..ef937fa 100644
--- a/sl1/format.go
+++ b/sl1/format.go
@@ -129,23 +129,23 @@ type ReadSeekCloser interface {
 	io.Closer
 }
 
-type Sl1 struct {
-	properties uv3dp.Properties
-	config     sl1Config
-	layerPng   []([]byte)
+type Print struct {
+	uv3dp.Print
+	config   sl1Config
+	layerPng []([]byte)
 }
 
-type Sl1Format struct {
+type Format struct {
 	*pflag.FlagSet
 
 	MaterialName string
 	BottomFade   bool
 }
 
-func NewSl1Formatter(suffix string) (sf *Sl1Format) {
+func NewFormatter(suffix string) (sf *Format) {
 	flagSet := pflag.NewFlagSet(suffix, pflag.ContinueOnError)
 
-	sf = &Sl1Format{
+	sf = &Format{
 		FlagSet: flagSet,
 	}
 
@@ -163,15 +163,14 @@ func sl1Timestamp() (stamp string) {
 	return
 }
 
-func (sf *Sl1Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err error) {
+func (sf *Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err error) {
 	archive := zip.NewWriter(writer)
 	defer archive.Close()
 
-	prop := printable.Properties()
-
-	size := &prop.Size
-	exp := &prop.Exposure
-	bot := &prop.Bottom.Exposure
+	size := printable.Size()
+	exp := printable.Exposure()
+	bot := printable.Bottom().Exposure
+	bot_count := printable.Bottom().Count
 
 	layerHeight := fmt.Sprintf("%.3g", size.LayerHeight)
 	materialName := sf.MaterialName
@@ -180,9 +179,9 @@ func (sf *Sl1Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 	}
 
 	numFade := 0
-	numSlow := prop.Bottom.Count
+	numSlow := bot_count
 	if sf.BottomFade {
-		numFade = prop.Bottom.Count
+		numFade = bot_count
 		numSlow = 0
 	}
 
@@ -198,7 +197,7 @@ func (sf *Sl1Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 		"numFast":               fmt.Sprintf("%v", size.Layers),
 		"numSlow":               fmt.Sprintf("%v", numSlow),
 		"printProfile":          layerHeight + " Normal",
-		"printTime":             fmt.Sprintf("%.3f", prop.Duration()),
+		"printTime":             fmt.Sprintf("%.3f", float32(uv3dp.PrintDuration(printable))/float32(time.Second)),
 		"printerModel":          "SL1",
 		"printerProfile":        "Original Prusa SL1",
 		"prusaSlicerVersion":    "uv3dp",
@@ -222,7 +221,7 @@ func (sf *Sl1Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 	}
 
 	// Create all the layers
-	uv3dp.WithEachLayer(printable, func(n int, layer uv3dp.Layer) {
+	uv3dp.WithEachLayer(printable, func(p uv3dp.Printable, n int) {
 		filename := fmt.Sprintf("%s%05d.png", config_ini["jobDir"], n)
 
 		var writer io.Writer
@@ -231,14 +230,23 @@ func (sf *Sl1Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 			return
 		}
 
-		err = png.Encode(writer, layer.Image)
+		err = png.Encode(writer, p.LayerImage(n))
 		if err != nil {
 			return
 		}
 	})
 
 	// Save the thumbnails
-	for _, image := range prop.Preview {
+	previews := []uv3dp.PreviewType{
+		uv3dp.PreviewTypeTiny,
+		uv3dp.PreviewTypeHuge,
+	}
+
+	for _, code := range previews {
+		image, ok := printable.Preview(code)
+		if !ok {
+			continue
+		}
 		imageSize := image.Bounds().Size()
 		filename := fmt.Sprintf("thumbnail/thumbnail%dx%d.png", imageSize.X, imageSize.Y)
 
@@ -257,7 +265,7 @@ func (sf *Sl1Format) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 	return
 }
 
-func (sf *Sl1Format) Decode(reader uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
+func (sf *Format) Decode(reader uv3dp.Reader, filesize int64) (printable uv3dp.Printable, err error) {
 	archive, err := zip.NewReader(reader, filesize)
 	if err != nil {
 		return
@@ -385,9 +393,9 @@ func (sf *Sl1Format) Decode(reader uv3dp.Reader, filesize int64) (printable uv3d
 
 	prop.Preview = thumbImage
 
-	sl1 := &Sl1{
-		properties: prop,
-		layerPng:   layerPng,
+	sl1 := &Print{
+		Print:    uv3dp.Print{Properties: prop},
+		layerPng: layerPng,
 	}
 
 	printable = sl1
@@ -395,23 +403,16 @@ func (sf *Sl1Format) Decode(reader uv3dp.Reader, filesize int64) (printable uv3d
 	return
 }
 
-func (sl1 *Sl1) Close() {
-}
-
-func (sl1 *Sl1) Properties() (prop uv3dp.Properties) {
-	prop = sl1.properties
-	return
+func (sl1 *Print) Close() {
 }
 
-func (sl1 *Sl1) Layer(index int) (layer uv3dp.Layer) {
+func (sl1 *Print) LayerImage(index int) (imageGray *image.Gray) {
 	pngImage, err := png.Decode(bytes.NewReader(sl1.layerPng[index]))
 	if err != nil {
 		panic(err)
 	}
 
-	layer.Z = sl1.properties.LayerZ(index)
-	layer.Image = pngImage.(*image.Gray)
-	layer.Exposure = sl1.properties.LayerExposure(index)
+	imageGray = pngImage.(*image.Gray)
 
 	return
 }
diff --git a/sl1/init.go b/sl1/init.go
index 640a627..d655fb8 100644
--- a/sl1/init.go
+++ b/sl1/init.go
@@ -16,7 +16,7 @@ var (
 )
 
 func init() {
-	newFormatter := func(suffix string) uv3dp.Formatter { return NewSl1Formatter(suffix) }
+	newFormatter := func(suffix string) uv3dp.Formatter { return NewFormatter(suffix) }
 
 	uv3dp.RegisterFormatter(".sl1", newFormatter)
 
diff --git a/uvj/format.go b/uvj/format.go
index 22662ce..634ef3b 100644
--- a/uvj/format.go
+++ b/uvj/format.go
@@ -43,13 +43,19 @@ func (e ErrConfigInvalid) Error() string {
 	return fmt.Sprintf("config.ini: Parameter '%s' invalid", string(e))
 }
 
+type UVJLayer struct {
+	Z        float32
+	Exposure uv3dp.Exposure
+}
+
 type UVJConfig struct {
 	Properties uv3dp.Properties
-	Layers     []uv3dp.Layer
+	Layers     []UVJLayer
 }
 
 type UVJ struct {
-	Config   UVJConfig
+	uv3dp.Print
+	Layers   []UVJLayer
 	layerPng []([]byte)
 }
 
@@ -73,11 +79,11 @@ func (sf *UVJFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 	archive := zip.NewWriter(writer)
 	defer archive.Close()
 
-	prop := printable.Properties()
-
-	// Don't encode the preview images into the config file
-	preview := prop.Preview
-	prop.Preview = nil
+	prop := uv3dp.Properties{
+		Size:     printable.Size(),
+		Exposure: printable.Exposure(),
+		Bottom:   printable.Bottom(),
+	}
 
 	// If LightPWM is set to 255, don't encode it
 	if prop.Exposure.LightPWM == 255 {
@@ -89,11 +95,11 @@ func (sf *UVJFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 
 	config := UVJConfig{
 		Properties: prop,
-		Layers:     make([]uv3dp.Layer, prop.Size.Layers),
+		Layers:     make([]UVJLayer, prop.Size.Layers),
 	}
 
 	// Create all the layers
-	uv3dp.WithEachLayer(printable, func(n int, layer uv3dp.Layer) {
+	uv3dp.WithEachLayer(printable, func(p uv3dp.Printable, n int) {
 		filename := fmt.Sprintf("slice/%08d.png", n)
 
 		var writer io.Writer
@@ -102,18 +108,22 @@ func (sf *UVJFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 			return
 		}
 
-		err = png.Encode(writer, layer.Image)
+		err = png.Encode(writer, p.LayerImage(n))
 		if err != nil {
 			return
 		}
 
+		exposure := p.LayerExposure(n)
+
 		// Trigger the JSON 'omitdefault' as needed
-		if layer.Exposure.LightPWM == 255 {
-			layer.Exposure.LightPWM = 0
+		if exposure.LightPWM == 255 {
+			exposure.LightPWM = 0
 		}
 
-		layer.Image = nil
-		config.Layers[n] = layer
+		config.Layers[n] = UVJLayer{
+			Z:        p.LayerZ(n),
+			Exposure: exposure,
+		}
 	})
 
 	// Create the config file
@@ -131,16 +141,23 @@ func (sf *UVJFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err
 	fileConfig.Write([]byte("\n"))
 
 	// Save the thumbnails
-	for code, image := range preview {
-		imageSize := image.Bounds().Size()
+	preview := []uv3dp.PreviewType{
+		uv3dp.PreviewTypeTiny,
+		uv3dp.PreviewTypeHuge,
+	}
+
+	for _, code := range preview {
+		image, ok := printable.Preview(code)
+		if !ok {
+			continue
+		}
+
 		var name string
 		switch code {
 		case uv3dp.PreviewTypeTiny:
 			name = "tiny"
 		case uv3dp.PreviewTypeHuge:
 			name = "huge"
-		default:
-			name = fmt.Sprintf("%dx%d", imageSize.X, imageSize.Y)
 		}
 
 		filename := "preview/" + name + ".png"
@@ -261,7 +278,8 @@ func (sf *UVJFormat) Decode(reader uv3dp.Reader, filesize int64) (printable uv3d
 	config.Properties.Preview = thumbImage
 
 	uvj := &UVJ{
-		Config:   config,
+		Print:    uv3dp.Print{Properties: config.Properties},
+		Layers:   config.Layers,
 		layerPng: layerPng,
 	}
 
@@ -273,26 +291,33 @@ func (sf *UVJFormat) Decode(reader uv3dp.Reader, filesize int64) (printable uv3d
 func (uvj *UVJ) Close() {
 }
 
-func (uvj *UVJ) Properties() (prop uv3dp.Properties) {
-	prop = uvj.Config.Properties
+func (uvj *UVJ) LayerZ(index int) (z float32) {
+	if len(uvj.Layers) == 0 {
+		z = uvj.Print.LayerZ(index)
+	} else {
+		z = uvj.Layers[index].Z
+	}
+
+	return
+}
+
+func (uvj *UVJ) LayerExposure(index int) (exposure uv3dp.Exposure) {
+	if len(uvj.Layers) == 0 {
+		exposure = uvj.Print.LayerExposure(index)
+	} else {
+		exposure = uvj.Layers[index].Exposure
+	}
+
 	return
 }
 
-func (uvj *UVJ) Layer(index int) (layer uv3dp.Layer) {
+func (uvj *UVJ) Image(index int) (layerImage *image.Gray) {
 	pngImage, err := png.Decode(bytes.NewReader(uvj.layerPng[index]))
 	if err != nil {
 		err = fmt.Errorf("Layer %v: %w", index, err)
 		panic(err)
 	}
 
-	if len(uvj.Config.Layers) == 0 {
-		layer = uv3dp.Layer{
-			Z:        uvj.Config.Properties.LayerZ(index),
-			Exposure: uvj.Config.Properties.LayerExposure(index),
-		}
-	} else {
-		layer = uvj.Config.Layers[index]
-	}
 	layerImage, ok := pngImage.(*image.Gray)
 	if !ok {
 		layerImage = image.NewGray(pngImage.Bounds())
@@ -303,7 +328,5 @@ func (uvj *UVJ) Layer(index int) (layer uv3dp.Layer) {
 		}
 	}
 
-	layer.Image = layerImage
-
 	return
 }
diff --git a/zcodex/format.go b/zcodex/format.go
index ab7d3a7..d029b3f 100644
--- a/zcodex/format.go
+++ b/zcodex/format.go
@@ -92,8 +92,8 @@ type ResinMetadata struct {
 }
 
 type Zcodex struct {
-	properties uv3dp.Properties
-	layerPng   []([]byte)
+	uv3dp.Print
+	layerPng []([]byte)
 }
 
 type ZcodexFormat struct {
@@ -113,13 +113,11 @@ func NewZcodexFormatter(suffix string) (sf *ZcodexFormat) {
 }
 
 func (sf *ZcodexFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (err error) {
-	prop := printable.Properties()
-
 	archive := zip.NewWriter(writer)
 	defer archive.Close()
 
 	var rm ResinMetadata
-	anon, ok := prop.Metadata["zcodex/ResinMetadata"]
+	anon, ok := printable.Metadata("zcodex/ResinMetadata")
 	if ok {
 		rmptr, ok := anon.(*ResinMetadata)
 		if ok {
@@ -127,15 +125,19 @@ func (sf *ZcodexFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (
 		}
 	}
 
-	rm.LayerThickness = prop.Size.LayerHeight
-	rm.LayerTime = int(prop.Exposure.LightOnTime * 1000.0)
-	rm.BottomLayersTime = int(prop.Bottom.Exposure.LightOnTime * 1000.0)
-	rm.TotalLayersCount = prop.Size.Layers
-	rm.BottomLayersNumber = prop.Bottom.Count
-	rm.BlankingLayerTime = int(prop.Exposure.LightOffTime * 1000.0)
+	size := printable.Size()
+	exposure := printable.Exposure()
+	bottom := printable.Bottom()
+
+	rm.LayerThickness = size.LayerHeight
+	rm.LayerTime = int(exposure.LightOnTime * 1000.0)
+	rm.BottomLayersTime = int(bottom.Exposure.LightOnTime * 1000.0)
+	rm.TotalLayersCount = size.Layers
+	rm.BottomLayersNumber = bottom.Count
+	rm.BlankingLayerTime = int(exposure.LightOffTime * 1000.0)
 
 	var us UserSettingsData
-	anon, ok = prop.Metadata["zcodex/UserSettingsData"]
+	anon, ok = printable.Metadata("zcodex/UserSettingsData")
 	if ok {
 		usptr, ok := anon.(*UserSettingsData)
 		if ok {
@@ -143,24 +145,20 @@ func (sf *ZcodexFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (
 		}
 	}
 
-	us.MaxLayer = prop.Size.Layers
-	us.LayerThickness = fmt.Sprintf("%.2g mm", prop.Size.LayerHeight)
-	us.LayerExposureTime = int(prop.Exposure.LightOnTime * 1000.0)
-	us.ExposureOffTime = int(prop.Exposure.LightOffTime * 1000.0)
-	us.BottomLayerExposureTime = int(prop.Bottom.Exposure.LightOnTime * 1000.0)
-	us.BottomLayersCount = prop.Bottom.Count
-	us.ZLiftDistance = prop.Exposure.LiftHeight
-	us.ZLiftRetractRate = prop.Exposure.RetractSpeed
-	us.ZLiftFeedRate = prop.Exposure.LiftSpeed
-
-	size := prop.Size
+	us.MaxLayer = size.Layers
+	us.LayerThickness = fmt.Sprintf("%.2g mm", size.LayerHeight)
+	us.LayerExposureTime = int(exposure.LightOnTime * 1000.0)
+	us.ExposureOffTime = int(exposure.LightOffTime * 1000.0)
+	us.BottomLayerExposureTime = int(bottom.Exposure.LightOnTime * 1000.0)
+	us.BottomLayersCount = bottom.Count
+	us.ZLiftDistance = exposure.LiftHeight
+	us.ZLiftRetractRate = exposure.RetractSpeed
+	us.ZLiftFeedRate = exposure.LiftSpeed
 
 	rm.Layers = make([]ResinMetadataLayer, size.Layers)
 
 	// Create all the layers
 	for n := 0; n < size.Layers; n++ {
-		layer := printable.Layer(n)
-
 		filename := fmt.Sprintf("ResinSlicesData/Slice%05d.png", n)
 
 		writer, err = archive.Create(filename)
@@ -168,7 +166,7 @@ func (sf *ZcodexFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (
 			return
 		}
 
-		err = png.Encode(writer, layer.Image)
+		err = png.Encode(writer, printable.LayerImage(n))
 		if err != nil {
 			return
 		}
@@ -199,9 +197,9 @@ func (sf *ZcodexFormat) Encode(writer uv3dp.Writer, printable uv3dp.Printable) (
 	}
 
 	// Save the thumbnails
-	image, ok := prop.Preview[uv3dp.PreviewTypeTiny]
+	image, ok := printable.Preview(uv3dp.PreviewTypeTiny)
 	if !ok {
-		image, ok = prop.Preview[uv3dp.PreviewTypeHuge]
+		image, ok = printable.Preview(uv3dp.PreviewTypeHuge)
 	}
 	if ok {
 		writer, err = archive.Create("Preview.png")
@@ -377,8 +375,8 @@ func (sf *ZcodexFormat) Decode(reader uv3dp.Reader, filesize int64) (printable u
 	prop.Metadata["zcodex/ResinMetadata"] = &rm
 
 	zcodex := &Zcodex{
-		properties: prop,
-		layerPng:   layerPng,
+		Print:    uv3dp.Print{Properties: prop},
+		layerPng: layerPng,
 	}
 
 	printable = zcodex
@@ -389,11 +387,6 @@ func (sf *ZcodexFormat) Decode(reader uv3dp.Reader, filesize int64) (printable u
 func (zcodex *Zcodex) Close() {
 }
 
-func (zcodex *Zcodex) Properties() (prop uv3dp.Properties) {
-	prop = zcodex.properties
-	return
-}
-
 func asGray(in image.Image) (out *image.Gray) {
 	bounds := in.Bounds()
 	out = image.NewGray(bounds)
@@ -407,7 +400,7 @@ func asGray(in image.Image) (out *image.Gray) {
 	return
 }
 
-func (zcodex *Zcodex) Layer(index int) (layer uv3dp.Layer) {
+func (zcodex *Zcodex) LayerImage(index int) (grayImage *image.Gray) {
 	pngImage, err := png.Decode(bytes.NewReader(zcodex.layerPng[index]))
 	if err != nil {
 		panic(err)
@@ -418,9 +411,5 @@ func (zcodex *Zcodex) Layer(index int) (layer uv3dp.Layer) {
 		grayImage = asGray(pngImage)
 	}
 
-	layer.Z = zcodex.properties.LayerZ(index)
-	layer.Image = grayImage
-	layer.Exposure = zcodex.properties.LayerExposure(index)
-
 	return
 }


Test patch:
diff --git a/cbddlp/format_alias_test.go b/cbddlp/format_alias_test.go
index c96b52e..da6ef49 100644
--- a/cbddlp/format_alias_test.go
+++ b/cbddlp/format_alias_test.go
@@ -27,17 +27,17 @@ type AliasPrintable struct {
 	uv3dp.Printable
 }
 
-func (ap *AliasPrintable) Layer(index int) (layer uv3dp.Layer) {
-	layer = ap.Printable.Layer(index)
+func (ap *AliasPrintable) LayerImage(index int) (ig *image.Gray) {
+	ig = ap.Printable.LayerImage(index)
 
-	layer.Image.Pix = greyMap
+	ig.Pix = greyMap
 
 	return
 }
 
 var (
 	// Collect an alias printable
-	aliasPrintable = &AliasPrintable{uv3dp.NewEmptyPrintable(uv3dp.Properties{
+	aliasPrintable = &AliasPrintable{&uv3dp.Print{uv3dp.Properties{
 		Size: uv3dp.Size{
 			X: 10,
 			Y: 1,
@@ -73,7 +73,7 @@ var (
 			uv3dp.PreviewTypeTiny: image.NewGray(image.Rect(0, 0, 1, 1)),
 			uv3dp.PreviewTypeHuge: image.NewGray(image.Rect(0, 0, 1, 1)),
 		},
-	})}
+	}}}
 )
 
 // reuse 'bufferMap' from format_empty_test.go
@@ -91,7 +91,7 @@ func TestAlias(t *testing.T) {
 	}
 
 	for n, item := range table {
-		formatter := NewCbddlpFormatter(".cbddlp")
+		formatter := NewFormatter(".cbddlp")
 		formatter.AntiAlias = item.AntiAlias
 
 		buffWriter := &bytes.Buffer{}
@@ -103,7 +103,7 @@ func TestAlias(t *testing.T) {
 	}
 
 	for _, item := range table {
-		formatter := NewCbddlpFormatter(".cbddlp")
+		formatter := NewFormatter(".cbddlp")
 
 		aliasRaw := item.Raw
 		buffReader := &bufferMap{Buffer: aliasRaw}
@@ -113,8 +113,16 @@ func TestAlias(t *testing.T) {
 			t.Fatalf("expected nil, got %v", err)
 		}
 
-		eProp := aliasPrintable.Properties()
-		rProp := result.Properties()
+		eProp := uv3dp.Properties{
+			Size:     aliasPrintable.Size(),
+			Exposure: aliasPrintable.Exposure(),
+			Bottom:   aliasPrintable.Bottom(),
+		}
+		rProp := uv3dp.Properties{
+			Size:     result.Size(),
+			Exposure: result.Exposure(),
+			Bottom:   result.Bottom(),
+		}
 
 		fixupV2(&eProp)
 		fixupV2(&rProp)
@@ -125,11 +133,11 @@ func TestAlias(t *testing.T) {
 			t.Logf("actual  : %+v", rProp)
 		}
 
-		rLayer := result.Layer(0)
-		if !cmp.Equal(rLayer.Image.Pix, item.GrayMap) {
+		rLayer := result.LayerImage(0)
+		if !cmp.Equal(rLayer.Pix, item.GrayMap) {
 			t.Errorf("aa%v: expected image to exactly match", item.AntiAlias)
 			t.Logf("expected: %+#v", item.GrayMap)
-			t.Logf("actual  : %+#v", rLayer.Image.Pix)
+			t.Logf("actual  : %+#v", rLayer.Pix)
 		}
 	}
 }
diff --git a/cbddlp/format_empty_test.go b/cbddlp/format_empty_test.go
index 4e14379..d3e11de 100644
--- a/cbddlp/format_empty_test.go
+++ b/cbddlp/format_empty_test.go
@@ -19,7 +19,7 @@ import (
 
 var (
 	// Collect an empty printable
-	emptyPrintable = uv3dp.NewEmptyPrintable(uv3dp.Properties{
+	emptyPrintable = &uv3dp.Print{uv3dp.Properties{
 		Size: uv3dp.Size{
 			X: 10,
 			Y: 20,
@@ -55,11 +55,11 @@ var (
 			uv3dp.PreviewTypeTiny: image.NewRGBA(image.Rect(0, 0, 10, 10)),
 			uv3dp.PreviewTypeHuge: image.NewCMYK(image.Rect(0, 0, 20, 12)),
 		},
-	})
+	}}
 
-	emptyRawV1 = []byte{0x19, 0x0, 0xfd, 0x12, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa0, 0x41, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x1b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x88, 0x40, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x70, 0x0, 0x0, 0x0, 0xb8, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x94, 0x0, 0x0, 0x0, 0x9f, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xef, 0x30, 0xa, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0xb4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x63, 0x30, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x10, 0x40, 0x48, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0xcc, 0x3d, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x10, 0x40, 0x48, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9a, 0x99, 0x19, 0x3e, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x88, 0x40, 0x48, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x88, 0x40, 0x48, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7d, 0x4b}
+	emptyRawV1 = []byte{0x19, 0x0, 0xfd, 0x12, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa0, 0x41, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x1b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x88, 0x40, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x70, 0x0, 0x0, 0x0, 0xb8, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x94, 0x0, 0x0, 0x0, 0xd1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xef, 0x30, 0xa, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0xb4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x63, 0x30, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x10, 0x40, 0x48, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0xcc, 0x3d, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x10, 0x40, 0x48, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9a, 0x99, 0x19, 0x3e, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x88, 0x40, 0x48, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x88, 0x40, 0x48, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7d, 0x4b}
 
-	emptyRawV2 = []byte{0x19, 0x0, 0xfd, 0x12, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa0, 0x41, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x1b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x88, 0x40, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x70, 0x0, 0x0, 0x0, 0xf4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x94, 0x0, 0x0, 0x0, 0x9f, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xb8, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x7b, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xef, 0x30, 0xa, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0xb4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x63, 0x30, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xb4, 0x42, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0x48, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x40, 0x0, 0x0, 0x88, 0x40, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x10, 0x40, 0x84, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0xcc, 0x3d, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x10, 0x40, 0x84, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9a, 0x99, 0x19, 0x3e, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x88, 0x40, 0x84, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x88, 0x40, 0x84, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7d, 0x4b}
+	emptyRawV2 = []byte{0x19, 0x0, 0xfd, 0x12, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa0, 0x41, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x1b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x88, 0x40, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x70, 0x0, 0x0, 0x0, 0xf4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x94, 0x0, 0x0, 0x0, 0xd1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xb8, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x7b, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xef, 0x30, 0xa, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0xb4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x63, 0x30, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xb4, 0x42, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0x48, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x40, 0x0, 0x0, 0x88, 0x40, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x10, 0x40, 0x84, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0xcc, 0x3d, 0x0, 0x0, 0x76, 0x42, 0x0, 0x0, 0x10, 0x40, 0x84, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9a, 0x99, 0x19, 0x3e, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x88, 0x40, 0x84, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0x38, 0x41, 0x0, 0x0, 0x88, 0x40, 0x84, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7d, 0x4b}
 )
 
 type bufferMap struct {
@@ -95,7 +95,7 @@ func TestEmptyToRaw(t *testing.T) {
 	}
 
 	for _, item := range table {
-		formatter := NewCbddlpFormatter(item.Format)
+		formatter := NewFormatter(item.Format)
 		formatter.AntiAlias = 1
 
 		buffWriter := &bytes.Buffer{}
@@ -155,7 +155,7 @@ func TestRawToEmpty(t *testing.T) {
 	}
 
 	for _, item := range table {
-		formatter := NewCbddlpFormatter(item.Format)
+		formatter := NewFormatter(item.Format)
 
 		emptyRaw := item.Raw
 		buffReader := &bufferMap{Buffer: emptyRaw}
@@ -165,8 +165,16 @@ func TestRawToEmpty(t *testing.T) {
 			t.Fatalf("expected nil, got %v", err)
 		}
 
-		eProp := emptyPrintable.Properties()
-		rProp := result.Properties()
+		eProp := uv3dp.Properties{
+			Size:     emptyPrintable.Size(),
+			Exposure: emptyPrintable.Exposure(),
+			Bottom:   emptyPrintable.Bottom(),
+		}
+		rProp := uv3dp.Properties{
+			Size:     result.Size(),
+			Exposure: result.Exposure(),
+			Bottom:   result.Bottom(),
+		}
 
 		// Remote non-savable values
 		item.Fixup(&eProp)
diff --git a/ctb/format_empty_test.go b/ctb/format_empty_test.go
index 09a0ce6..1149a12 100644
--- a/ctb/format_empty_test.go
+++ b/ctb/format_empty_test.go
@@ -18,7 +18,7 @@ import (
 
 var (
 	// Collect an empty printable
-	emptyPrintable = uv3dp.NewEmptyPrintable(uv3dp.Properties{
+	emptyPrintable = &uv3dp.Print{uv3dp.Properties{
 		Size: uv3dp.Size{
 			X: 10,
 			Y: 20,
@@ -54,9 +54,9 @@ var (
 			uv3dp.PreviewTypeTiny: image.NewRGBA(image.Rect(0, 0, 10, 10)),
 			uv3dp.PreviewTypeHuge: image.NewCMYK(image.Rect(0, 0, 20, 12)),
 		},
-	})
+	}}
 
-	emptyRaw = []byte{0x86, 0x0, 0xfd, 0x12, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa0, 0x41, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x1b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x70, 0x0, 0x0, 0x0, 0x47, 0x1, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x94, 0x0, 0x0, 0x0, 0x4b, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xb8, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x42, 0x4, 0xcb, 0x9a, 0xf4, 0x0, 0x0, 0x0, 0x4c, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xef, 0x30, 0xa, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0xb4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x63, 0x30, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0x48, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x40, 0x0, 0x0, 0x10, 0x40, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x34, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x78, 0x56, 0x34, 0x12, 0x1, 0x0, 0x0, 0x0, 0x1, 0x6, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xd7, 0x1, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0xcc, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xda, 0x1, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9a, 0x99, 0x19, 0x3e, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xdd, 0x1, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xe0, 0x1, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0x61, 0x23, 0x7e, 0x35, 0x46, 0xfd, 0xa, 0xf9, 0x7c, 0xde, 0x1c}
+	emptyRaw = []byte{0x86, 0x0, 0xfd, 0x12, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa0, 0x41, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x1b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x70, 0x0, 0x0, 0x0, 0x47, 0x1, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x94, 0x0, 0x0, 0x0, 0x6a, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xb8, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x42, 0x4, 0xcb, 0x9a, 0xf4, 0x0, 0x0, 0x0, 0x4c, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xef, 0x30, 0xa, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0xb4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x63, 0x30, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0x48, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x40, 0x0, 0x0, 0x10, 0x40, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x34, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x1, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x78, 0x56, 0x34, 0x12, 0x1, 0x0, 0x0, 0x0, 0x1, 0x6, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xd7, 0x1, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0xcc, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xda, 0x1, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9a, 0x99, 0x19, 0x3e, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xdd, 0x1, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xe0, 0x1, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0x61, 0x23, 0x7e, 0x35, 0x46, 0xfd, 0xa, 0xf9, 0x7c, 0xde, 0x1c}
 )
 
 type bufferMap struct {
@@ -91,7 +91,7 @@ func TestEmptyToRaw(t *testing.T) {
 	}
 
 	for _, item := range table {
-		formatter := NewCtbFormatter(item.Format)
+		formatter := NewFormatter(item.Format)
 
 		buffWriter := &bytes.Buffer{}
 		formatter.Encode(buffWriter, emptyPrintable)
@@ -153,7 +153,7 @@ func TestRawToEmpty(t *testing.T) {
 	}
 
 	for _, item := range table {
-		formatter := NewCtbFormatter(item.Format)
+		formatter := NewFormatter(item.Format)
 
 		emptyRaw := item.Raw
 		buffReader := &bufferMap{Buffer: emptyRaw}
@@ -163,8 +163,16 @@ func TestRawToEmpty(t *testing.T) {
 			t.Fatalf("expected nil, got %v", err)
 		}
 
-		eProp := emptyPrintable.Properties()
-		rProp := result.Properties()
+		eProp := uv3dp.Properties{
+			Size:     emptyPrintable.Size(),
+			Exposure: emptyPrintable.Exposure(),
+			Bottom:   emptyPrintable.Bottom(),
+		}
+		rProp := uv3dp.Properties{
+			Size:     result.Size(),
+			Exposure: result.Exposure(),
+			Bottom:   result.Bottom(),
+		}
 
 		// Remote non-savable values
 		item.Fixup(&eProp)
@@ -177,20 +185,26 @@ func TestRawToEmpty(t *testing.T) {
 		}
 
 		for n := 0; n < eProp.Size.Layers; n++ {
-			rLayer := result.Layer(n)
-			eLayer := emptyPrintable.Layer(n)
+			rLayerZ := result.LayerZ(n)
+			eLayerZ := emptyPrintable.LayerZ(n)
 
-			if rLayer.Z != eLayer.Z {
-				t.Errorf("layer %d: expected Z %f did not match result Z %f", n, eLayer.Z, rLayer.Z)
+			if rLayerZ != eLayerZ {
+				t.Errorf("layer %d: expected Z %f did not match result Z %f", n, eLayerZ, rLayerZ)
 			}
 
-			if !cmp.Equal(eLayer.Exposure, rLayer.Exposure) {
+			rLayerExposure := result.LayerExposure(n)
+			eLayerExposure := emptyPrintable.LayerExposure(n)
+
+			if !cmp.Equal(eLayerExposure, rLayerExposure) {
 				t.Errorf("layer %d: expected exposure did not match result exposure", n)
-				t.Logf("expect: %+v", eLayer.Exposure)
-				t.Logf("result: %+v", rLayer.Exposure)
+				t.Logf("expect: %+v", eLayerExposure)
+				t.Logf("result: %+v", rLayerExposure)
 			}
 
-			if !imageEqual(eLayer.Image, rLayer.Image) {
+			rLayerImage := result.LayerImage(n)
+			eLayerImage := emptyPrintable.LayerImage(n)
+
+			if !imageEqual(eLayerImage, rLayerImage) {
 				t.Errorf("layer %d: images did not match", n)
 			}
 		}
diff --git a/cws/format_test.go b/cws/format_test.go
index 596d059..68f3f29 100644
--- a/cws/format_test.go
+++ b/cws/format_test.go
@@ -177,7 +177,7 @@ func TestEncodeEmptyCWS(t *testing.T) {
 
 	empty := uv3dp.NewEmptyPrintable(testProperties)
 
-	formatter := NewCWSFormatter(".cws")
+	formatter := NewFormatter(".cws")
 
 	buffWriter := &bytes.Buffer{}
 	formatter.Encode(buffWriter, empty)
diff --git a/phz/format_empty_test.go b/phz/format_empty_test.go
index e4f67e4..cdb3a17 100644
--- a/phz/format_empty_test.go
+++ b/phz/format_empty_test.go
@@ -56,7 +56,7 @@ var (
 		},
 	})
 
-	emptyRaw = []byte{0xae, 0x83, 0xda, 0x9f, 0x2, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x84, 0x41, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xd8, 0x0, 0x0, 0x0, 0x27, 0x1, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0xfc, 0x0, 0x0, 0x0, 0x4b, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0xa0, 0x41, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x1b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x40, 0x0, 0x0, 0x10, 0x40, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0x48, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x1, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x6, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0xf8, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xef, 0x30, 0xa, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x1c, 0x1, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x63, 0x30, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xb7, 0x1, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0xcc, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xb7, 0x1, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9a, 0x99, 0x19, 0x3e, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xb7, 0x1, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xb7, 0x1, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4}
+	emptyRaw = []byte{0xae, 0x83, 0xda, 0x9f, 0x2, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x84, 0x41, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xd8, 0x0, 0x0, 0x0, 0x27, 0x1, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0xfc, 0x0, 0x0, 0x0, 0x6a, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xff, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0xa0, 0x41, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x1b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x40, 0x0, 0x0, 0x10, 0x40, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0xb0, 0x40, 0x0, 0x0, 0xf0, 0x42, 0x0, 0x0, 0x48, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x1, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x6, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0xf8, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xef, 0x30, 0xa, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x1c, 0x1, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x63, 0x30, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0xcd, 0xcc, 0x4c, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xb7, 0x1, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0xcc, 0x3d, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xb7, 0x1, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9a, 0x99, 0x19, 0x3e, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xb7, 0x1, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xcd, 0xcc, 0x4c, 0x3e, 0x0, 0x0, 0x84, 0x41, 0x0, 0x0, 0x10, 0x40, 0xb7, 0x1, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4, 0x80, 0x4}
 )
 
 type bufferMap struct {
@@ -91,7 +91,7 @@ func TestEmptyToRaw(t *testing.T) {
 	}
 
 	for _, item := range table {
-		formatter := NewPhzFormatter(item.Format)
+		formatter := NewFormatter(item.Format)
 
 		buffWriter := &bytes.Buffer{}
 		formatter.Encode(buffWriter, emptyPrintable)
@@ -153,7 +153,7 @@ func TestRawToEmpty(t *testing.T) {
 	}
 
 	for _, item := range table {
-		formatter := NewPhzFormatter(item.Format)
+		formatter := NewFormatter(item.Format)
 
 		emptyRaw := item.Raw
 		buffReader := &bufferMap{Buffer: emptyRaw}
@@ -163,8 +163,16 @@ func TestRawToEmpty(t *testing.T) {
 			t.Fatalf("expected nil, got %v", err)
 		}
 
-		eProp := emptyPrintable.Properties()
-		rProp := result.Properties()
+		eProp := uv3dp.Properties{
+			Size:     emptyPrintable.Size(),
+			Exposure: emptyPrintable.Exposure(),
+			Bottom:   emptyPrintable.Bottom(),
+		}
+		rProp := uv3dp.Properties{
+			Size:     result.Size(),
+			Exposure: result.Exposure(),
+			Bottom:   result.Bottom(),
+		}
 
 		// Remote non-savable values
 		item.Fixup(&eProp)
@@ -177,20 +185,26 @@ func TestRawToEmpty(t *testing.T) {
 		}
 
 		for n := 0; n < eProp.Size.Layers; n++ {
-			rLayer := result.Layer(n)
-			eLayer := emptyPrintable.Layer(n)
+			rLayerZ := result.LayerZ(n)
+			eLayerZ := emptyPrintable.LayerZ(n)
 
-			if rLayer.Z != eLayer.Z {
-				t.Errorf("layer %d: expected Z %f did not match result Z %f", n, eLayer.Z, rLayer.Z)
+			if rLayerZ != eLayerZ {
+				t.Errorf("layer %d: expected Z %f did not match result Z %f", n, eLayerZ, rLayerZ)
 			}
 
-			if !cmp.Equal(eLayer.Exposure, rLayer.Exposure) {
+			rLayerExposure := result.LayerExposure(n)
+			eLayerExposure := emptyPrintable.LayerExposure(n)
+
+			if !cmp.Equal(eLayerExposure, rLayerExposure) {
 				t.Errorf("layer %d: expected exposure did not match result exposure", n)
-				t.Logf("%+v", eLayer.Exposure)
-				t.Logf("%+v", rLayer.Exposure)
+				t.Logf("%+v", eLayerExposure)
+				t.Logf("%+v", rLayerExposure)
 			}
 
-			if !imageEqual(eLayer.Image, rLayer.Image) {
+			rLayerImage := result.LayerImage(n)
+			eLayerImage := emptyPrintable.LayerImage(n)
+
+			if !imageEqual(eLayerImage, rLayerImage) {
 				t.Errorf("layer %d: images did not match", n)
 			}
 		}
diff --git a/pws/format_test.go b/pws/format_test.go
index 2710aa2..84c90a5 100644
--- a/pws/format_test.go
+++ b/pws/format_test.go
@@ -135,8 +135,16 @@ func TestRawToEmpty(t *testing.T) {
 			t.Fatalf("expected nil, got %v", err)
 		}
 
-		eProp := emptyPrintable.Properties()
-		rProp := result.Properties()
+		eProp := uv3dp.Properties{
+			Size:     emptyPrintable.Size(),
+			Exposure: emptyPrintable.Exposure(),
+			Bottom:   emptyPrintable.Bottom(),
+		}
+		rProp := uv3dp.Properties{
+			Size:     result.Size(),
+			Exposure: result.Exposure(),
+			Bottom:   result.Bottom(),
+		}
 
 		eProp.Preview = nil
 		rProp.Preview = nil
diff --git a/sl1/format_test.go b/sl1/format_test.go
index 5e94e0a..1c69638 100644
--- a/sl1/format_test.go
+++ b/sl1/format_test.go
@@ -84,7 +84,7 @@ numFade = 2
 numFast = 4
 numSlow = 0
 printProfile = 0.05 Normal
-printTime = 75.425
+printTime = 100.520
 printerModel = SL1
 printerProfile = Original Prusa SL1
 prusaSlicerVersion = uv3dp
@@ -110,7 +110,7 @@ func TestEncodeEmptySl1(t *testing.T) {
 
 	empty := uv3dp.NewEmptyPrintable(testProperties)
 
-	formatter := NewSl1Formatter(".sl1")
+	formatter := NewFormatter(".sl1")
 	formatter.BottomFade = true
 
 	buffWriter := &bytes.Buffer{}
diff --git a/uvj/uvj2uvj_test.go b/uvj/uvj2uvj_test.go
index abba69a..1dffe33 100644
--- a/uvj/uvj2uvj_test.go
+++ b/uvj/uvj2uvj_test.go
@@ -51,15 +51,13 @@ func TestUVJToUVJ(t *testing.T) {
 		t.Fatalf("can't read %v: %v", ReferenceUVJ, err)
 	}
 
-	uvj_prop := uvj_print.Properties()
-	out_prop := out_print.Properties()
-	if uvj_prop.Size != out_prop.Size {
+	if uvj_print.Size() != out_print.Size() {
 		t.Errorf("printables are not the same Size")
 	}
-	if uvj_prop.Bottom != out_prop.Bottom {
+	if uvj_print.Bottom() != out_print.Bottom() {
 		t.Errorf("printables are not the same Bottom exposure")
 	}
-	if uvj_prop.Exposure != out_prop.Exposure {
+	if uvj_print.Exposure() != out_print.Exposure() {
 		t.Errorf("printables are not the same Exposure")
 	}
 
@@ -69,8 +67,8 @@ func TestUVJToUVJ(t *testing.T) {
 	}
 
 	for _, code := range thumbnails {
-		uvj_prev, uvj_ok := uvj_prop.Preview[code]
-		out_prev, out_ok := out_prop.Preview[code]
+		uvj_prev, uvj_ok := uvj_print.Preview(code)
+		out_prev, out_ok := out_print.Preview(code)
 
 		if uvj_ok != out_ok {
 			t.Errorf("%+v: expected present %v, got %v", code, uvj_ok, out_ok)