diff --git a/go.mod b/go.mod index a9d1ec4..1084ee6 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/galaco/vtf + +go 1.13 diff --git a/reader.go b/reader.go index 06f467d..c0e2ded 100644 --- a/reader.go +++ b/reader.go @@ -9,14 +9,37 @@ import ( "io" ) -const headerSize = 96 +const ( + vtfSignature = "VTF\x00" +) + +var ( + // ErrorVtfSignatureMismatch occurs when a stream does not start with the VTF magic signature + ErrorVtfSignatureMismatch = errors.New("header signature does not match: VTF\x00") + // ErrorTextureDepthNotSupported occurs when attempting to parse a stream with depth > 1 + ErrorTextureDepthNotSupported = errors.New("only vtf textures with depth 1 are supported") + // ErrorMipmapSizeMismatch occurs when filesize does not match calculated mipmap size + ErrorMipmapSizeMismatch = errors.New("expected data size is smaller than actual") +) -// Reader: Vtf Reader +// Reader reads from a vtf stream type Reader struct { stream io.Reader } -// Read: Reads the vtf image from stream into a usable structure +// ReadHeader reads the header of a texture only. +func (reader *Reader) ReadHeader() (*Header, error) { + buf := bytes.Buffer{} + _, err := buf.ReadFrom(reader.stream) + if err != nil { + return nil, err + } + + // Header + return reader.parseHeader(buf.Bytes()) +} + +// Read parses vtf image from stream into a usable structure // The only error to expect would be if mipmap data size overflows the total file size; normally // due to tampered Header data. func (reader *Reader) Read() (*Vtf, error) { @@ -32,7 +55,7 @@ func (reader *Reader) Read() (*Vtf, error) { return nil, err } - // Tesources - in vtf 7.3+ only + // Resources - in vtf 7.3+ only resourceData, err := reader.parseOtherResourceData(header, buf.Bytes()) if err != nil { return nil, err @@ -45,7 +68,7 @@ func (reader *Reader) Read() (*Vtf, error) { } // Mipmaps - highResImage, err := reader.readMipmaps(header, buf.Bytes()[header.HeaderSize+uint32(len(lowResImage)):]) + highResImage, err := reader.readMipmaps(header, buf.Bytes()) if err != nil { return nil, err } @@ -58,12 +81,11 @@ func (reader *Reader) Read() (*Vtf, error) { }, nil } -// parseHeader: Parse vtf Header. +// parseHeader reads vtf Header. func (reader *Reader) parseHeader(buffer []byte) (*Header, error) { - - // We know Header is 96 bytes + // We know Header is 96 bytes maximum // Note it *may* not be someday... - headerBytes := make([]byte, headerSize) + headerBytes := make([]byte, 96) // Read bytes equal to Header size byteReader := bytes.NewReader(buffer) @@ -79,14 +101,14 @@ func (reader *Reader) parseHeader(buffer []byte) (*Header, error) { if err != nil { return nil, err } - if string(header.Signature[:4]) != "VTF\x00" { - return nil, errors.New("Header signature does not match: VTF\x00") + if string(header.Signature[:4]) != vtfSignature { + return nil, ErrorVtfSignatureMismatch } return &header, nil } -// parseOtherResourceData: Returns resource data for 7.3+ images +// parseOtherResourceData reads resource data for 7.3+ images func (reader *Reader) parseOtherResourceData(header *Header, buffer []byte) ([]byte, error) { // validate Header version if (header.Version[0]*10+header.Version[1] < 73) || header.NumResource == 0 { @@ -98,7 +120,7 @@ func (reader *Reader) parseOtherResourceData(header *Header, buffer []byte) ([]b return []byte{}, nil } -// readLowResolutionMipmap: Reads the low resolution texture information +// readLowResolutionMipmap reads the low resolution texture information // This is normally what you see previewed in Hammer texture browser. // The largest axis should always be 16 wide/tall. The smallest can be any value, // but is padded out to divisible by 4 for Dxt1 compressionn reasons @@ -109,7 +131,7 @@ func (reader *Reader) readLowResolutionMipmap(header *Header, buffer []byte) ([] format.Dxt1) imgBuffer := make([]byte, bufferSize) - byteReader := bytes.NewReader(buffer[headerSize : headerSize+bufferSize]) + byteReader := bytes.NewReader(buffer[int(header.HeaderSize) : int(header.HeaderSize)+bufferSize]) sectionReader := io.NewSectionReader(byteReader, 0, int64(bufferSize)) _, err := sectionReader.Read(imgBuffer) if err != nil { @@ -119,39 +141,39 @@ func (reader *Reader) readLowResolutionMipmap(header *Header, buffer []byte) ([] return imgBuffer, nil } -// readMipmaps: Read all mipmaps +// readMipmaps reads all mipmaps // Returned format is a bit odd, but is just a set of flat arrays containing arrays: // mipmap[frame[face[slice[RGBA]]] -func (reader *Reader) readMipmaps(header *Header, buffer []byte) ([][][][][]byte, error) { +func (reader *Reader) readMipmaps(header *Header, buffer []byte) ([][][][][]uint8, error) { if header.Depth > 1 { - return [][][][][]byte{}, errors.New("only vtf textures with depth 1 are supported") + return [][][][][]uint8{}, ErrorTextureDepthNotSupported } depth := header.Depth - // This shouldn't ever happen, yet it occasionally seems to + // Occurs in texture versions before 7.2 if depth == 0 { depth = 1 } // Only support 1 ZSlice. No known Source game can use > 1 zslices numZSlice := uint16(1) - bufferOffset := 0 + bufferEnd := len(buffer) storedFormat := format.Format(header.HighResImageFormat) mipmapSizes := internal.ComputeMipmapSizes(int(header.MipmapCount), int(header.Width), int(header.Height)) // Iterate mipmap; smallest to largest - mipMaps := make([][][][][]byte, header.MipmapCount) - for mipmapIdx := uint8(0); mipmapIdx < header.MipmapCount; mipmapIdx++ { + mipMaps := make([][][][][]uint8, header.MipmapCount) + for mipmapIdx := int8(header.MipmapCount - 1); mipmapIdx >= int8(0); mipmapIdx-- { // Frame by frame; first to last - frames := make([][][][]byte, header.Frames) + frames := make([][][][]uint8, header.Frames) for frameIdx := uint16(0); frameIdx < header.Frames; frameIdx++ { - faces := make([][][]byte, 1) + faces := make([][][]uint8, 1) // Face by face; first to last // @TODO is this correct to use depth? How to know how many faces there are for faceIdx := uint16(0); faceIdx < depth; faceIdx++ { - zSlices := make([][]byte, 1) + zSlices := make([][]uint8, 1) // Z Slice by Z Slice; first to last // @TODO wtf is a z slice, and how do we know how many there are for sliceIdx := uint16(0); sliceIdx < numZSlice; sliceIdx++ { @@ -159,12 +181,12 @@ func (reader *Reader) readMipmaps(header *Header, buffer []byte) ([][][][][]byte mipmapSizes[mipmapIdx][0], mipmapSizes[mipmapIdx][1], storedFormat) - if len(buffer) < bufferOffset+bufferSize { - return mipMaps, errors.New("expected data size is smaller than actual") + if len(buffer) < bufferEnd-bufferSize { + return mipMaps, ErrorMipmapSizeMismatch } - img := buffer[bufferOffset : bufferOffset+bufferSize] + img := buffer[bufferEnd-bufferSize : bufferEnd] - bufferOffset += bufferSize + bufferEnd -= bufferSize zSlices[sliceIdx] = img } faces[faceIdx] = zSlices diff --git a/vtf.go b/vtf.go index 78fd0eb..5de9c00 100644 --- a/vtf.go +++ b/vtf.go @@ -6,7 +6,7 @@ type Vtf struct { header Header resources []byte lowResolutionImageData []uint8 - highResolutionImageData [][][][][]byte //[]mipmap[]frame[]face[]slice + highResolutionImageData [][][][][]uint8 //[]mipmap[]frame[]face[]slice } // Header returns vtf Header @@ -20,13 +20,18 @@ func (vtf *Vtf) LowResImageData() []uint8 { } // HighResImageData returns all data for all mipmaps -func (vtf *Vtf) HighResImageData() [][][][][]byte { +func (vtf *Vtf) HighResImageData() [][][][][]uint8 { return vtf.highResolutionImageData } +// Image returns raw data of the first frame of the highest resolution mipmap +func (vtf *Vtf) Image() []uint8 { + return vtf.HighestResolutionImageForFrame(0) +} + // MipmapsForFrame returns all mipmap sizes for a single frame -func (vtf *Vtf) MipmapsForFrame(frame int) [][]byte { - ret := make([][]byte, vtf.header.MipmapCount) +func (vtf *Vtf) MipmapsForFrame(frame int) [][]uint8 { + ret := make([][]uint8, vtf.header.MipmapCount) for idx, mipmap := range vtf.highResolutionImageData { ret[idx] = mipmap[frame][0][0]