-
-
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.