Last active
October 11, 2022 20:28
-
-
Save deckarep/d09e98827945208db3bd4770f1256235 to your computer and use it in GitHub Desktop.
KQ6 - Hires Portrait Extractor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
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
@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
@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.