コード例 #1
0
ファイル: wii.py プロジェクト: Illidanz/hacktools
def getFontGlyphs(file):
    glyphs = {}
    with common.Stream(file, "rb", False) as f:
        # Header
        f.seek(36)
        hdwcoffset = f.readUInt()
        pamcoffset = f.readUInt()
        common.logDebug("hdwcoffset:", hdwcoffset, "pamcoffset:", pamcoffset)
        # HDWC
        f.seek(hdwcoffset - 4)
        hdwclen = f.readUInt()
        tilenum = (hdwclen - 16) // 3
        firstcode = f.readUShort()
        lastcode = f.readUShort()
        f.seek(4, 1)
        common.logDebug("firstcode:", firstcode, "lastcode:", lastcode,
                        "tilenum", tilenum)
        hdwc = []
        for i in range(tilenum):
            hdwcstart = f.readSByte()
            hdwcwidth = f.readByte()
            hdwclength = f.readByte()
            hdwc.append((hdwcstart, hdwcwidth, hdwclength))
        # PAMC
        nextoffset = pamcoffset
        while nextoffset != 0x00:
            f.seek(nextoffset)
            firstchar = f.readUShort()
            lastchar = f.readUShort()
            sectiontype = f.readUShort()
            f.seek(2, 1)
            nextoffset = f.readUInt()
            common.logDebug("firstchar:", common.toHex(firstchar), "lastchar:",
                            common.toHex(lastchar), "sectiontype:",
                            sectiontype, "nextoffset:", nextoffset)
            if sectiontype == 0:
                firstcode = f.readUShort()
                for i in range(lastchar - firstchar + 1):
                    c = common.codeToChar(firstchar + i)
                    glyphs[c] = common.FontGlyph(hdwc[firstcode + i][0],
                                                 hdwc[firstcode + i][1],
                                                 hdwc[firstcode + i][2], c,
                                                 firstchar + i, firstcode + i)
            elif sectiontype == 1:
                for i in range(lastchar - firstchar + 1):
                    charcode = f.readUShort()
                    if charcode == 0xFFFF or charcode >= len(hdwc):
                        continue
                    c = common.codeToChar(firstchar + i)
                    glyphs[c] = common.FontGlyph(hdwc[charcode][0],
                                                 hdwc[charcode][1],
                                                 hdwc[charcode][2], c,
                                                 firstchar + i, charcode)
            else:
                common.logWarning("Unknown section type", sectiontype)
    return glyphs
コード例 #2
0
ファイル: psx.py プロジェクト: Illidanz/hacktools
def drawTIM(outfile,
            tim,
            transp=False,
            forcepal=-1,
            allpalettes=False,
            nopal=False):
    if tim.width == 0 or tim.height == 0:
        return
    clutwidth = clutheight = 0
    if tim.bpp == 4 or tim.bpp == 8:
        clut = forcepal if forcepal != -1 else getUniqueCLUT(tim, transp)
        if not nopal:
            clutwidth = 40
            clutheight = 5 * (len(tim.cluts[clut]) // 8)
            if allpalettes:
                clutheight *= len(tim.cluts)
    img = Image.new("RGBA",
                    (tim.width + clutwidth, max(tim.height, clutheight)),
                    (0, 0, 0, 0))
    pixels = img.load()
    x = 0
    for i in range(tim.height):
        for j in range(tim.width):
            if x >= len(tim.data):
                common.logWarning("Out of TIM data")
                break
            if tim.bpp == 4 or tim.bpp == 8:
                if len(tim.cluts[clut]) > tim.data[x]:
                    color = tim.cluts[clut][tim.data[x]]
                else:
                    common.logWarning("Index", tim.data[x], "not in CLUT")
                    color = (0, 0, 0, 0)
            else:
                color = tim.data[x]
            if not transp:
                color = (color[0], color[1], color[2], 255)
            pixels[j, i] = color
            x += 1
    if (tim.bpp == 4 or tim.bpp == 8) and not nopal:
        if allpalettes:
            for i in range(len(tim.cluts)):
                pixels = common.drawPalette(pixels, tim.cluts[i], tim.width,
                                            i * (clutheight // len(tim.cluts)),
                                            transp)
        else:
            pixels = common.drawPalette(pixels, tim.cluts[clut], tim.width, 0,
                                        transp)
    if outfile == "":
        return img
    img.save(outfile, "PNG")
コード例 #3
0
ファイル: psx.py プロジェクト: Illidanz/hacktools
def readTIMData(f, tim, pixelnum):
    try:
        for i in range(pixelnum):
            if tim.bpp == 4:
                tim.data.append(f.readHalf())
            elif tim.bpp == 8:
                tim.data.append(f.readByte())
            elif tim.bpp == 16:
                color = common.readRGB5A1(f.readUShort())
                tim.data.append(color)
            elif tim.bpp == 24:
                tim.data.append(
                    (f.readByte(), f.readByte(), f.readByte(), 255))
    except struct.error:
        common.logWarning("Malformed TIM")
コード例 #4
0
ファイル: game.py プロジェクト: Illidanz/OokamiTranslation
def readShiftJIS(f, len2=False, untilZero=False, encoding="shift_jis"):
    if untilZero:
        strlen2 = 999
    else:
        if len2:
            strlen = f.readUShort()
            strlen2 = f.readUShort()
        else:
            strlen = f.readByte()
            strlen2 = f.readByte()
    sjis = ""
    i = j = 0
    padding = 0
    while i < strlen2:
        b1 = f.readByte()
        if b1 == 0x00:
            i += 1
            j += 1
            padding += 1
            if untilZero:
                return sjis, i
        else:
            b2 = f.readByte()
            if b1 == 0x0d and b2 == 0x0a:
                sjis += "|"
                i += 2
                j += 2
            elif b1 == 0x81 and b2 == 0xa5:
                sjis += ">>"
                i += 2
                j += 1
            elif not common.checkShiftJIS(b1, b2):
                f.seek(-1, 1)
                sjis += chr(b1)
                i += 1
                j += 1
            else:
                f.seek(-2, 1)
                try:
                    sjis += f.read(2).decode(encoding).replace("〜", "~")
                except UnicodeDecodeError:
                    common.logDebug("UnicodeDecodeError at", f.tell() - 2)
                    sjis += "UNK(" + common.toHex(b1) + common.toHex(b2) + ")"
                i += 2
                j += 1
    if not untilZero and j != strlen:
        common.logWarning("Wrong strlen", strlen, j)
    return sjis, i
コード例 #5
0
ファイル: game.py プロジェクト: Illidanz/GurrenTranslation
def writeShiftJIS(f, str, writelen=True, maxlen=0):
    if str == "":
        if writelen:
            f.writeShort(1)
        f.writeByte(0)
        return 1
    i = 0
    strlen = 0
    if writelen:
        lenpos = f.tell()
        f.writeShort(strlen)
    if ord(str[0]) < 256 or str[0] == "“" or str[0] == "”" or str[0] == "↓":
        # ASCII string
        while i < len(str):
            # Add a space if the next character is <XX>, UNK(XXXX) or CUS(XXXX)
            if i < len(str) - 1 and str[i+1] == "<":
                str = str[:i+1] + " " + str[i+1:]
            elif i < len(str) - 4 and (str[i+1:i+5] == "UNK(" or str[i+1:i+5] == "CUS("):
                str = str[:i+1] + " " + str[i+1:]
            char = str[i]
            # Code format <XX>
            if char == "<" and i < len(str) - 3 and str[i+3] == ">":
                try:
                    if maxlen > 0 and strlen + 1 > maxlen:
                        return -1
                    code = str[i+1] + str[i+2]
                    f.write(bytes.fromhex(code))
                    strlen += 1
                except ValueError:
                    common.logwarning("Invalid escape code", str[i+1], str[i+2])
                i += 4
            # Unknown format UNK(XXXX)
            elif char == "U" and i < len(str) - 4 and str[i:i+4] == "UNK(":
                if maxlen > 0 and strlen + 2 > maxlen:
                    return -1
                code = str[i+4] + str[i+5]
                f.write(bytes.fromhex(code))
                code = str[i+6] + str[i+7]
                f.write(bytes.fromhex(code))
                i += 9
                strlen += 2
            # Custom full-size glyph CUS(XXXX)
            elif char == "C" and i < len(str) - 4 and str[i:i+4] == "CUS(":
                if maxlen > 0 and strlen + 2 > maxlen:
                    return -1
                f.write(bytes.fromhex(common.table[str[i+4:i+8]]))
                i += 9
                strlen += 2
            else:
                if i + 1 == len(str):
                    bigram = char + " "
                else:
                    bigram = char + str[i+1]
                i += 2
                if maxlen > 0 and strlen + 2 > maxlen:
                    return -1
                if bigram not in common.table:
                    try:
                        common.logWarning("Bigram not found:", bigram, "in string", str)
                    except UnicodeEncodeError:
                        common.logWarning("Bigram not found in string", str)
                    bigram = "  "
                f.write(bytes.fromhex(common.table[bigram]))
                strlen += 2
    else:
        # SJIS string
        str = str.replace("~", "〜")
        while i < len(str):
            char = str[i]
            if char == "<":
                code = str[i+1] + str[i+2]
                i += 4
                f.write(bytes.fromhex(code))
                strlen += 1
            else:
                i += 1
                f.write(char.encode("shift-jis"))
                strlen += 2
    if writelen:
        f.writeZero(1)
        pos = f.tell()
        f.seek(lenpos)
        f.writeShort(strlen + 1)
        f.seek(pos)
    return strlen + 1
コード例 #6
0
ファイル: game.py プロジェクト: Illidanz/GurrenTranslation
def drawMappedImage(width, height, mapdata, tiledata, paldata, tilesize=8, bpp=4):
    palnum = len(paldata) // 32
    img = Image.new("RGBA", (width + 40, max(height, palnum * 10)), (0, 0, 0, 0))
    pixels = img.load()
    # Maps
    maps = []
    for i in range(0, len(mapdata), 2):
        map = struct.unpack("<h", mapdata[i:i+2])[0]
        pal = (map >> 12) & 0xF
        xflip = (map >> 10) & 1
        yflip = (map >> 11) & 1
        tile = map & 0x3FF
        maps.append((pal, xflip, yflip, tile))
    common.logDebug("Loaded", len(maps), "maps")
    # Tiles
    tiles = []
    for i in range(len(tiledata) // (32 if bpp == 4 else 64)):
        singletile = []
        for j in range(tilesize * tilesize):
            x = i * (tilesize * tilesize) + j
            if bpp == 4:
                index = (tiledata[x // 2] >> ((x % 2) << 2)) & 0x0f
            else:
                index = tiledata[x]
            singletile.append(index)
        tiles.append(singletile)
    common.logDebug("Loaded", len(tiles), "tiles")
    # Palette
    palettes = readPaletteData(paldata)
    pals = []
    for palette in palettes:
        pals += palette
    # Draw the image
    i = j = 0
    for map in maps:
        try:
            pal = map[0]
            xflip = map[1]
            yflip = map[2]
            tile = tiles[map[3]]
            for i2 in range(tilesize):
                for j2 in range(tilesize):
                    pixels[j + j2, i + i2] = pals[16 * pal + tile[i2 * tilesize + j2]]
            # Very inefficient way to flip pixels
            if xflip or yflip:
                sub = img.crop(box=(j, i, j + tilesize, i + tilesize))
                if yflip:
                    sub = ImageOps.flip(sub)
                if xflip:
                    sub = ImageOps.mirror(sub)
                img.paste(sub, box=(j, i))
        except (KeyError, IndexError):
            common.logWarning("Tile or palette", str(map), "not found")
        j += tilesize
        if j >= width:
            j = 0
            i += tilesize
    # Draw palette
    if len(palettes) > 0:
        for i in range(len(palettes)):
            pixels = common.drawPalette(pixels, palettes[i], width, i * 10)
    return img
コード例 #7
0
def run():
    binin = "data/extract/arm9.bin"
    binout = "data/repack/arm9.bin"
    binfile = "data/bin_input.txt"
    tablefile = "data/table.txt"
    if not os.path.isfile(binfile):
        common.logError("Input file", binfile, "not found.")
        return
    common.copyFile(binin, binout)

    freeranges = [(0xEA810, 0xEEC00)]
    currentrange = 0
    rangepos = 0

    section = {}
    with codecs.open(binfile, "r", "utf-8") as bin:
        section = common.getSection(bin, "", "#", game.fixchars)
        chartot, transtot = common.getSectionPercentage(section)

    common.logMessage("Repacking BIN from", binfile, "...")
    common.loadTable(tablefile)
    rangepos = freeranges[currentrange][0]
    with common.Stream(binin, "rb") as fi:
        allbin = fi.read()
        strpointers = {}
        with common.Stream(binout, "r+b") as fo:
            # Skip the beginning and end of the file to avoid false-positives
            fi.seek(992000)
            while fi.tell() < 1180000:
                pos = fi.tell()
                if pos < 1010000 or pos > 1107700:
                    check = game.detectShiftJIS(fi)
                    if check in section and section[check][0] != "":
                        common.logDebug("Replacing string at", pos)
                        newstr = section[check][0]
                        # Check how much padding space we have
                        padding = 0
                        while True:
                            if fi.readByte() == 0x00:
                                padding += 1
                            else:
                                fi.seek(-1, 1)
                                break
                        fo.seek(pos)
                        endpos = fi.tell() - 1
                        newlen = game.writeShiftJIS(fo, newstr, False,
                                                    endpos - pos)
                        if newlen < 0:
                            if rangepos >= freeranges[currentrange][
                                    1] and newstr not in strpointers:
                                common.logWarning("No more room! Skipping ...")
                            else:
                                # Write the string in a new portion of the rom
                                if newstr in strpointers:
                                    newpointer = strpointers[newstr]
                                else:
                                    common.logDebug(
                                        "No room for the string, redirecting to",
                                        rangepos)
                                    fo.seek(rangepos)
                                    game.writeShiftJIS(fo, newstr, False)
                                    fo.writeZero(1)
                                    newpointer = 0x02000000 + rangepos
                                    rangepos = fo.tell()
                                    strpointers[newstr] = newpointer
                                    if rangepos >= freeranges[currentrange][1]:
                                        if currentrange + 1 < len(freeranges):
                                            currentrange += 1
                                            rangepos = freeranges[
                                                currentrange][0]
                                # Search and replace the old pointer
                                pointer = 0x02000000 + pos
                                pointersearch = struct.pack("<I", pointer)
                                index = 0
                                common.logDebug("Searching for pointer",
                                                pointersearch.hex().upper())
                                while index < len(allbin):
                                    index = allbin.find(pointersearch, index)
                                    if index < 0:
                                        break
                                    common.logDebug("  Replaced pointer at",
                                                    str(index))
                                    fo.seek(index)
                                    fo.writeUInt(newpointer)
                                    index += 4
                        else:
                            fo.writeZero(endpos - fo.tell())
                    if check != "":
                        pos = fi.tell() - 1
                fi.seek(pos + 1)
    common.logMessage("Done! Translation is at {0:.2f}%".format(
        (100 * transtot) / chartot))
コード例 #8
0
def run(firstgame, no_redirect):
    infolder = "data/extract/data/data/"
    outfolder = "data/repack/data/data/"
    infile = "data/dat_input.txt"
    redfile = "data/redirects.asm"
    fontfile = "data/replace/data/font/lcfont12.NFTR"
    if not os.path.isfile(infile):
        common.logError("Input file", infile, "not found")
        return
    common.makeFolder(outfolder)
    chartot = transtot = 0
    monthsection, skipsection = game.monthsection, game.skipsection
    game.monthsection = game.skipsection = None

    encoding = "shift_jis" if firstgame else "shift_jisx0213"
    common.logMessage("Repacking DAT from", infile, "...")
    # Read the glyph size from the font
    if not os.path.isfile(fontfile):
        fontfile = fontfile.replace("replace/", "extract/")
    glyphs = nitro.readNFTR(fontfile).glyphs
    fixchars = game.getFixChars()
    # Copy this txt file
    if not firstgame and os.path.isfile(infolder + "facilityhelp.txt"):
        common.copyFile(infolder + "facilityhelp.txt",
                        outfolder + "facilityhelp.txt")
    redirects = []
    with codecs.open(infile, "r", "utf-8") as dat:
        files = common.getFiles(infolder, ".dat")
        for file in common.showProgress(files):
            section = common.getSection(dat, file, fixchars=fixchars)
            # If there are no lines, just copy the file
            if len(section) == 0:
                common.copyFile(infolder + file, outfolder + file)
                # Part of the AP patch
                if not firstgame and file == "route.dat":
                    with common.Stream(outfolder + file, "rb+") as f:
                        f.seek(0x5ee8)
                        f.writeByte(0x0)
                continue
            i = 0
            chartot, transtot = common.getSectionPercentage(
                section, chartot, transtot)
            common.logDebug("Processing", file, "...")
            size = os.path.getsize(infolder + file)
            with common.Stream(infolder + file, "rb") as fin:
                with common.Stream(outfolder + file, "wb") as f:
                    f.write(fin.read())
                    fin.seek(0)
                    # Loop the file and replace strings as needed
                    while fin.tell() < size - 2:
                        pos = fin.tell()
                        check = game.detectShiftJIS(fin, encoding)
                        if check != "":
                            if file == "entrance_icon.dat":
                                # For entrance_icon, just write the string and update the pointer
                                if check in section:
                                    # For the first one, seek to the correct position in the output file
                                    if i == 0:
                                        f.seek(pos)
                                    # Write the string
                                    newsjis = check
                                    if check in section and section[check][
                                            0] != "":
                                        common.logDebug(
                                            "Replacing string at", pos)
                                        newsjis = section[check][0]
                                    startpos = f.tell()
                                    game.writeShiftJIS(f, newsjis, False, True,
                                                       0, encoding)
                                    endpos = f.tell()
                                    # Update the pointer
                                    f.seek(0x1c98 + 4 * i)
                                    f.writeUInt(startpos - 0x1c98)
                                    f.seek(endpos)
                                    i += 1
                            else:
                                # Found a SJIS string, check if we have to replace it
                                if check in section and section[check][0] != "":
                                    common.logDebug("Replacing string at", pos)
                                    f.seek(pos)
                                    newsjis = section[check][0]
                                    maxlen = 0
                                    if file == "goods.dat":
                                        newsjis = common.wordwrap(
                                            newsjis, glyphs, 170)
                                        maxlen = 60
                                    elif file == "gossip.dat":
                                        newsjis = common.wordwrap(
                                            newsjis, glyphs, 190)
                                        if newsjis.count("<<") > 0:
                                            newsjis = common.centerLines(
                                                newsjis,
                                                glyphs,
                                                190,
                                                centercode="<<")
                                        if fin.tell() - pos < 35:
                                            maxlen = 35
                                        else:
                                            maxlen = 160
                                    elif file == "scenarioguide.dat":
                                        newsjis = common.wordwrap(
                                            newsjis, glyphs, 165)
                                        maxlen = 60
                                        if newsjis.count("|") > 1:
                                            common.logError(
                                                "scenarioguide line", newsjis,
                                                "too long")
                                    newlen = game.writeShiftJIS(
                                        f, newsjis, False, True, maxlen,
                                        encoding)
                                    if newlen < 0:
                                        if file != "gossip.dat" or no_redirect or maxlen != 160:
                                            common.logError(
                                                "String {} is too long ({}/{})."
                                                .format(
                                                    newsjis, len(newsjis),
                                                    maxlen))
                                        else:
                                            common.logWarning(
                                                "String {} is too long ({}/{})."
                                                .format(
                                                    newsjis, len(newsjis),
                                                    maxlen))
                                            # Doesn't fit, write it shorter
                                            f.seek(pos)
                                            cutat = 155 if firstgame else 150
                                            while ord(newsjis[cutat]) > 127:
                                                cutat -= 1
                                            stringfit = newsjis[:cutat]
                                            stringrest = newsjis[cutat:]
                                            game.writeShiftJIS(
                                                f, stringfit, False, True,
                                                maxlen, encoding)
                                            f.seek(-1, 1)
                                            f.writeByte(0x1f)
                                            f.writeByte(len(redirects))
                                            redirects.append(stringrest)
                                    # Pad with 0s if the line is shorter
                                    while f.tell() < fin.tell():
                                        f.writeByte(0x00)
                            pos = fin.tell() - 1
                        fin.seek(pos + 1)
    with codecs.open(redfile, "w", "utf-8") as f:
        f.write(".ascii \"NDSC\"\n\n")
        f.write("REDIRECT_START:\n\n")
        for i in range(len(redirects)):
            f.write(".dh REDIRECT_{} - REDIRECT_START\n".format(i))
        for i in range(len(redirects)):
            f.write("\nREDIRECT_{}:\n".format(i))
            redirect = redirects[i].replace("\"", "\\\"")
            redirect = redirect.replace("|", "\" :: .db 0xa :: .ascii \"")
            redirectascii = ""
            for c in redirect:
                if ord(c) > 127:
                    sjisc = common.toHex(
                        int.from_bytes(c.encode(encoding), "big"))
                    redirectascii += "\" :: .db 0x" + sjisc[:2] + " :: .db 0x" + sjisc[
                        2:] + " :: .ascii \""
                else:
                    redirectascii += c
            f.write(".ascii \"{}\" :: .db 0\n".format(redirectascii))
    game.monthsection, game.skipsection = monthsection, skipsection
    common.logMessage("Done! Translation is at {0:.2f}%".format(
        (100 * transtot) / chartot))
コード例 #9
0
def readGIMBlock(f, gim, image):
    offset = f.tell()
    id = f.readUShort()
    f.seek(2, 1)
    if id == 0xFF:
        # Info block
        common.logDebug("GIM 0xFF at", common.toHex(offset))
        return 0, image
    elif id == 0x03:
        # Picture block
        common.logDebug("GIM 0x03 at", common.toHex(offset))
        image = GIMImage()
        gim.images.append(image)
        image.picoff = offset
        image.picsize = f.readUInt()
        nextblock = f.readUInt()
        common.logDebug("picoff", image.picoff, "picsize", image.picsize)
        return image.picoff + nextblock, image
    elif id == 0x04:
        # Image block
        common.logDebug("GIM 0x04 at", common.toHex(offset))
        image.imgoff = offset
        image.imgsize = f.readUInt()
        nextblock = f.readUInt()
        f.seek(4, 1)
        image.imgframeoff = f.readUShort()
        f.seek(2, 1)
        image.format = f.readUShort()
        if image.format == 0x04:
            image.bpp = 4
        elif image.format == 0x05:
            image.bpp = 8
        elif image.format == 0x03 or image.format == 0x07:
            image.bpp = 32
        else:
            image.bpp = 16
        image.tiled = f.readUShort()
        image.width = f.readUShort()
        image.height = f.readUShort()
        if image.tiled == 0x01:
            image.tilewidth = 0x80 // image.bpp
            image.tileheight = 8
            image.blockedwidth = math.ceil(
                image.width / image.tilewidth) * image.tilewidth
            image.blockedheight = math.ceil(
                image.height / image.tileheight) * image.tileheight
        if image.format > 0x05:
            common.logError("Unsupported image format:", image.format)
            return image.imgoff + nextblock, image
        f.seek(image.imgoff + 32 + image.imgframeoff)
        for i in range(image.blockedheight if image.tiled ==
                       0x01 else image.height):
            for j in range(image.blockedwidth if image.tiled ==
                           0x01 else image.width):
                index = 0
                if image.format == 0x04:
                    index = f.readHalf()
                elif image.format == 0x05:
                    index = f.readByte()
                else:
                    index = readColor(f, image.format)
                image.colors.append(index)
        common.logDebug("imgoff", image.imgoff, "imgsize", image.imgsize,
                        "imgframeoff", image.imgframeoff, "format",
                        image.format, "bpp", image.bpp)
        common.logDebug("tiled", image.tiled, "width", image.width, "height",
                        image.height)
        common.logDebug("blockedwidth", image.blockedwidth, "blockedheight",
                        image.blockedheight, "tilewidth", image.tilewidth,
                        "tileheight", image.tileheight)
        return image.imgoff + nextblock, image
    elif id == 0x05:
        # Palette
        common.logDebug("GIM 0x05 at", common.toHex(offset))
        image.paloff = offset
        image.palsize = f.readUInt()
        nextblock = f.readUInt()
        f.seek(4, 1)
        image.palframeoff = f.readUShort()
        f.seek(2, 1)
        image.palformat = f.readUShort()
        f.seek(image.paloff + 32 + image.palframeoff)
        while f.tell() < image.paloff + nextblock:
            image.palette.append(readColor(f, image.palformat))
        common.logDebug("paloff", image.paloff, "palsize", image.palsize,
                        "palframeoff", image.palframeoff)
        common.logDebug("palformat", image.palformat, "length",
                        len(image.palette))
        return image.paloff + nextblock, image
    else:
        common.logWarning("Skipping unknown block at", offset, ":", id)
        f.seek(4, 1)
        return offset + f.readUInt(), image