Repository: qmuntal/gltf
Pull request #34 (2021-02-25 17:12:55)

Support .glb files without a binary buffer 
The `gltf` spec allows to have a `.glb` file without a binary buffer so that all buffers are either encoded as `base-64` or pointing to an external resource. https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#chunks.

The current implementation does not honor this use case and always skips the first buffer, even if it is not a binary buffer.

With this change the decoder will check if the first buffer is an binary buffer, and only if it is it will add the binary chunk to the file.




Patch:
diff --git a/encode.go b/encode.go
index d0a82f9..4c4bffb 100644
--- a/encode.go
+++ b/encode.go
@@ -8,7 +8,6 @@ import (
 	"io"
 	"os"
 	"path/filepath"
-	"unsafe"
 )
 
 // WriteHandler is the interface that wraps the Write method.
@@ -73,8 +72,11 @@ func (e *Encoder) Encode(doc *Document) error {
 	var err error
 	var externalBufferIndex = 0
 	if e.AsBinary {
-		err = e.encodeBinary(doc)
-		externalBufferIndex = 1
+		var hasBinChunk bool
+		hasBinChunk, err = e.encodeBinary(doc)
+		if hasBinChunk {
+			externalBufferIndex = 1
+		}
 	} else {
 		err = json.NewEncoder(e.w).Encode(doc)
 	}
@@ -103,44 +105,54 @@ func (e *Encoder) encodeBuffer(buffer *Buffer) error {
 	return e.WriteHandler.WriteResource(buffer.URI, buffer.Data)
 }
 
-func (e *Encoder) encodeBinary(doc *Document) error {
+func (e *Encoder) encodeBinary(doc *Document) (bool, error) {
 	jsonText, err := json.Marshal(doc)
 	if err != nil {
-		return err
+		return false, err
 	}
-	header := glbHeader{Magic: glbHeaderMagic, Version: 2, Length: 0, JSONHeader: chunkHeader{Length: 0, Type: glbChunkJSON}}
-	binHeader := chunkHeader{Length: 0, Type: glbChunkBIN}
-	var binBufferLength uint32
-	var binBuffer *Buffer
-	if len(doc.Buffers) > 0 {
-		binBuffer = doc.Buffers[0]
-		binBufferLength = binBuffer.ByteLength
+	jsonHeader := chunkHeader{
+		Length: uint32(((len(jsonText) + 3) / 4) * 4),
+		Type:   glbChunkJSON,
+	}
+	header := glbHeader{
+		Magic:      glbHeaderMagic,
+		Version:    2,
+		Length:     12 + 8 + jsonHeader.Length, // 12-byte glb header + 8-byte json chunk header
+		JSONHeader: jsonHeader,
 	}
-	binPaddedLength := ((binBufferLength + 3) / 4) * 4
-	binPadding := make([]byte, binPaddedLength-binBufferLength)
-	binHeader.Length = binPaddedLength
-
-	header.JSONHeader.Length = uint32(((len(jsonText) + 3) / 4) * 4)
-	header.Length = uint32(unsafe.Sizeof(header)+unsafe.Sizeof(binHeader)) + header.JSONHeader.Length + binHeader.Length
 	headerPadding := make([]byte, header.JSONHeader.Length-uint32(len(jsonText)))
 	for i := range headerPadding {
 		headerPadding[i] = ' '
 	}
-	for i := range binPadding {
-		binPadding[i] = 0
-	}
 	err = binary.Write(e.w, binary.LittleEndian, &header)
 	if err != nil {
-		return err
+		return false, err
 	}
 	e.w.Write(jsonText)
 	e.w.Write(headerPadding)
-	binary.Write(e.w, binary.LittleEndian, &binHeader)
-	if binBuffer != nil {
+
+	hasBinChunk := len(doc.Buffers) > 0 && doc.Buffers[0].URI == ""
+	if hasBinChunk {
+		var binBufferLength uint32
+		var binBuffer *Buffer
+		if len(doc.Buffers) > 0 {
+			binBuffer = doc.Buffers[0]
+			binBufferLength = binBuffer.ByteLength
+		}
+		binPaddedLength := ((binBufferLength + 3) / 4) * 4
+		binPadding := make([]byte, binPaddedLength-binBufferLength)
+		binHeader := chunkHeader{Length: 0, Type: glbChunkBIN}
+		binHeader.Length = binPaddedLength
+		header.Length = uint32(8) + binHeader.Length
+		for i := range binPadding {
+			binPadding[i] = 0
+		}
+		binary.Write(e.w, binary.LittleEndian, &binHeader)
 		e.w.Write(binBuffer.Data)
+		_, err = e.w.Write(binPadding)
 	}
-	_, err = e.w.Write(binPadding)
-	return err
+
+	return hasBinChunk, err
 }
 
 // UnmarshalJSON unmarshal the node with the correct default values.


Test patch:
diff --git a/encode_test.go b/encode_test.go
index ed46990..46430b8 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"reflect"
+	"strings"
 	"testing"
 
 	"github.com/go-test/deep"
@@ -36,6 +37,52 @@ func saveMemory(doc *Document, asBinary bool) (*Decoder, error) {
 	return NewDecoder(buff).WithReadHandler(m), nil
 }
 
+func TestEncoder_Encode_AsBinary_WithoutBuffer(t *testing.T) {
+	doc := &Document{}
+	buff := new(bytes.Buffer)
+	e := NewEncoder(buff)
+	e.AsBinary = true
+	if err := e.Encode(doc); err != nil {
+		t.Errorf("Encoder.Encode() error = %v", err)
+	}
+	if strings.Contains(buff.String(), "BIN") {
+		t.Error("Encoder.Encode() as binary without bin buffer should not contain bin chunk")
+	}
+}
+
+func TestEncoder_Encode_AsBinary_WithoutBinChunk(t *testing.T) {
+	doc := &Document{Buffers: []*Buffer{
+		{Extras: 8.0, Name: "embedded", ByteLength: 2, URI: "data:application/octet-stream;base64,YW55ICsgb2xkICYgZGF0YQ==", Data: []byte("any + old & data")},
+		{Extras: 8.0, Name: "external", ByteLength: 4, URI: "b.bin", Data: []byte{4, 5, 6, 7}},
+		{Extras: 8.0, Name: "external", ByteLength: 4, URI: "a.drc", Data: []byte{0, 0, 0, 0}},
+	}}
+	buff := new(bytes.Buffer)
+	m := &mockChunkReadHandler{Chunks: make(map[string][]byte)}
+	e := NewEncoder(buff).WithWriteHandler(m)
+	e.AsBinary = true
+	if err := e.Encode(doc); err != nil {
+		t.Errorf("Encoder.Encode() error = %v", err)
+	}
+	if strings.Contains(buff.String(), "BIN") {
+		t.Error("Encoder.Encode() as binary without bin buffer should not contain bin chunk")
+	}
+}
+
+func TestEncoder_Encode_AsBinary_WithBinChunk(t *testing.T) {
+	doc := &Document{Buffers: []*Buffer{
+		{Extras: 8.0, Name: "binary", ByteLength: 3, Data: []byte{1, 2, 3}},
+	}}
+	buff := new(bytes.Buffer)
+	e := NewEncoder(buff)
+	e.AsBinary = true
+	if err := e.Encode(doc); err != nil {
+		t.Errorf("Encoder.Encode() error = %v", err)
+	}
+	if !strings.Contains(buff.String(), "BIN") {
+		t.Error("Encoder.Encode() as binary with bin buffer should contain bin chunk")
+	}
+}
+
 func TestEncoder_Encode(t *testing.T) {
 	type args struct {
 		doc *Document
@@ -72,12 +119,6 @@ func TestEncoder_Encode(t *testing.T) {
 				{Extras: 8.0, Input: Index(1), Output: Index(1), Interpolation: InterpolationCubicSpline},
 			}},
 		}}}, false},
-		{"withBuffer", args{&Document{Buffers: []*Buffer{
-			{Extras: 8.0, Name: "binary", ByteLength: 3, URI: "a.bin", Data: []byte{1, 2, 3}},
-			{Extras: 8.0, Name: "embedded", ByteLength: 2, URI: "data:application/octet-stream;base64,YW55ICsgb2xkICYgZGF0YQ==", Data: []byte("any + old & data")},
-			{Extras: 8.0, Name: "external", ByteLength: 4, URI: "b.bin", Data: []byte{4, 5, 6, 7}},
-			{Extras: 8.0, Name: "external", ByteLength: 4, URI: "a.drc", Data: []byte{0, 0, 0, 0}},
-		}}}, false},
 		{"withBufView", args{&Document{BufferViews: []*BufferView{
 			{Extras: 8.0, Buffer: 0, ByteOffset: 1, ByteLength: 2, ByteStride: 5, Target: TargetArrayBuffer},
 			{Buffer: 10, ByteOffset: 10, ByteLength: 20, ByteStride: 50, Target: TargetElementArrayBuffer},