Skip to content

Instantly share code, notes, and snippets.

@deckarep
Last active October 11, 2022 20:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save deckarep/d09e98827945208db3bd4770f1256235 to your computer and use it in GitHub Desktop.
Save deckarep/d09e98827945208db3bd4770f1256235 to your computer and use it in GitHub Desktop.
KQ6 - Hires Portrait Extractor
package main
import (
"encoding/binary"
"fmt"
"image"
"image/color"
"image/png"
"io/ioutil"
"log"
"os"
"strings"
)
// Note: quick and dirty extraction script to pull out hires portraits from KQ6
// Usage: `go run main.go` in the directory of the .BIN files.
// Output: It will spit out all the relevant .png files
// ScummVM: https://github.com/scummvm/scummvm/blob/90f2ff2532ca71033b4393b9ce604c9b0e6cafa0/engines/sci/graphics/portrait.cpp
// REFERENCED: referenced: Portrait::init and Portrait::drawBitmap
// NOTE: None of the Rave, Lip-Sync stuff was implemented...don't need it.
type colors struct {
r byte
g byte
b byte
}
var portraits = []string{
"ALEX.BIN",
"ALLARIA.BIN",
//"ALLARIAD.BIN", // Redundant
"BEAST.BIN",
"BEAUTESS.BIN",
"BEAUTPEA.BIN",
"BOOKSH.BIN",
"BOOKWORM.BIN",
//"CALIPHID.BIN", // Redundant
"CALIPHIM.BIN",
"CASSIMA.BIN",
"CELESTE.BIN",
"FERRYM.BIN",
"GNOMES.BIN",
"GRAHAM.BIN",
"HEADDRU.BIN",
"JOLLO.BIN",
"LAMPSELL.BIN",
"PAWNSHOP.BIN",
"PRINCE.BIN",
"ROSELLA.BIN",
"SALADIN.BIN",
"SIGHT.BIN",
"SMELL.BIN",
//"SMELLNO.BIN", // Redundant
"SOUND.BIN",
//"SOUNDNO.BIN", // Redundant
"TASTE.BIN",
"TOUCH.BIN",
"VALANICE.BIN",
"VIZIER.BIN",
"WINGG.BIN",
}
func main() {
for _, fileName := range portraits {
processPortrait(fileName)
}
}
func processPortrait(fileName string) {
b, err := ioutil.ReadFile("ACTORS/" + fileName)
if err != nil {
log.Fatal("Couldn't open file with err!")
}
// Header
winHeader := string(b[0:3])
if winHeader != "WIN" {
log.Fatal("WIN Header not detected!")
}
// These ones commented out are kinda redundant.
//width := binary.LittleEndian.Uint16(b[3:])
//height := binary.LittleEndian.Uint16(b[5:])
bitmapSize := binary.LittleEndian.Uint16(b[7:])
//lipSyncIDCount := binary.LittleEndian.Uint16(b[11:])
portraitPaletteSize := binary.LittleEndian.Uint16(b[13:])
//fmt.Println(width, height, bitmapSize, lipSyncIDCount, portraitPaletteSize)
// Palette: starts at offset 17
var dataOffset = 17
palette := make([]colors, portraitPaletteSize)
var palSize uint16
var palNr uint16
for palSize < portraitPaletteSize {
palette[palNr].b = b[dataOffset]
dataOffset++
palette[palNr].g = b[dataOffset]
dataOffset++
palette[palNr].r = b[dataOffset]
dataOffset++
// ScummVM has these two lines hardcoded.
// _portraitPalette.colors[palNr].used = 1
// _portraitPalette.intensity[palNr] = 100
palNr += 1
palSize += 3
}
// Bitmap
var bitmapNr uint16
var bytesPerLine uint16
// Note: should only need FIRST bitmap in sequence.
for bitmapNr = 0; bitmapNr < bitmapSize; bitmapNr++ {
// Hmm width/height here redundant?
curWidth := binary.LittleEndian.Uint16(b[dataOffset+2:])
curHeight := binary.LittleEndian.Uint16(b[dataOffset+4:])
bytesPerLine = binary.LittleEndian.Uint16(b[dataOffset+6:])
if bytesPerLine < curWidth {
log.Fatal("Bitmap width larger than bytesPerLine!")
}
extraBytesPerLine := bytesPerLine - curWidth
rawBitmap := b[dataOffset+14 : dataOffset+14+int(curWidth*curHeight)]
//fmt.Println("bitmapNr:", bitmapNr, "extraBytesPerLine:", extraBytesPerLine, "len(rawBitmap):", len(rawBitmap))
exportBitmap(fileName, bitmapNr, rawBitmap, palette, curWidth, curHeight, extraBytesPerLine)
// Move dataOffset forward
dataOffset += int(14 + (curHeight * bytesPerLine))
}
}
func exportBitmap(fileName string, nr uint16, bitmap []byte, palette []colors, width uint16, height uint16, extraBytesPerLine uint16) {
myImg := image.NewRGBA(image.Rect(0, 0, int(width), int(height)))
dataOffset := 0
reducedBitmap := bitmap[0 : (width+extraBytesPerLine)*height]
for y := 0; y < int(height); y++ {
for x := 0; x < int(width); x++ {
c := palette[reducedBitmap[dataOffset]]
myImg.SetRGBA(x, y, color.RGBA{
R: c.r,
G: c.g,
B: c.b,
A: 255,
})
dataOffset += 1
}
dataOffset += int(extraBytesPerLine)
}
newName := strings.Replace(fileName, ".BIN", "", -1)
out, err := os.Create(fmt.Sprintf(newName+"_%d.png", nr))
if err != nil {
log.Fatal(err)
}
err = png.Encode(out, myImg)
if err != nil {
log.Fatal(err)
}
err = out.Close()
if err != nil {
log.Fatal(err)
}
}
@deckarep
Copy link
Author

deckarep commented Oct 9, 2022

Thanks @Doomlazer for hunting the bug! I confirmed ALL embedded bitmaps are now exporting correctly.

@Doomlazer
Copy link

You probably should swap bytesPerLine and curWidth back to the way they were. I don't know why I trusted the SVM comments that width was at 6 when I knew it was incorrect. A lesson for me about rejecting fact for belief, I guess.

@deckarep
Copy link
Author

@Doomlazer - hmmm it's no problem, I've run into this scenario before where the comments may not be accurate. As far as I know right now though, the code as-is in this latest gist exports all photos.

When I have time I can look in detail at what the correct approach is.

@Doomlazer
Copy link

As I incorrectly changed the names, I decided to clean it up for you. I've tested this is still working, but uses the more correct naming conventions. I guess you can't PR on gist, but you can copy paste from here: https://gist.github.com/Doomlazer/d06247a108a85511b4782885af61484e

@deckarep
Copy link
Author

deckarep commented Oct 11, 2022

@Doomlazer - I appreciate that and I’ve updated this gist to rev 4 (applied go fmt) to reflect the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment