-
-
Save deckarep/d09e98827945208db3bd4770f1256235 to your computer and use it in GitHub Desktop.
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) | |
} | |
} |
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.
@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.
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.
Thanks @Doomlazer for hunting the bug! I confirmed ALL embedded bitmaps are now exporting correctly.