#!/usr/bin/python
# -*- coding: utf-8 -*-
##
# Project: gExtractWinIcons
# Extract cursors and icons from MS Windows compatible resource files.
# Author: Fabio Castelli <muflone@vbsimple.net>
# Copyright: 2009-2010 Fabio Castelli
# License: GPL-2+
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# On Debian GNU/Linux systems, the full text of the GNU General Public License
# can be found in the file /usr/share/common-licenses/GPL-2.
##
import gtk
import gtk.glade
import pygtk
import subprocess
import tempfile
import os
import sys
import shutil
import gettext
from gettext import gettext as _
from optparse import OptionParser
__file_path__ = os.path.dirname(os.path.abspath(__file__))
APP_NAME = 'gextractwinicons'
APP_TITLE = 'gExtractWinIcons'
APP_VERSION = '0.3.1'
PATHS = {
'locale': [
'%s/po' % __file_path__,
'%s/share/locale' % sys.prefix],
'data': [
'%s/data' % __file_path__,
'%s/share/%s/data' % (sys.prefix, APP_NAME)],
'doc': [
'%s/doc' % __file_path__,
'%s/share/doc/%s' % (sys.prefix, APP_NAME)]
}
def getPath(key, append = ''):
"Returns the correct path for the specified key"
for path in PATHS[key]:
if os.path.isdir(path):
if append:
return os.path.join(path, append)
else:
return path
APP_LOGO = getPath('data', '%s.svg' % APP_NAME)
def readTextFile(filename):
"Read a text file and return its content"
try:
f = open(filename, 'r')
text = f.read()
f.close()
except:
text = ''
return text
def updateTotals():
"Update totals on the label"
lblTotals.set_label(strTotalsTemplate % (intTotalResources, intSelectedResources))
def refreshTotals():
"Write totals and update buttons"
updateTotals()
set_enabled(btnSelectAll, intTotalResources>0)
set_enabled(btnDeselectAll, intTotalResources>0)
set_enabled(btnSelectPNG, intTotalResources>0)
set_enabled(btnSaveResources, intTotalResources>0)
def extractResource(line):
"Extract resource from wrestool output"
resName = ''
resType = ''
resLang = ''
# Split line in fields
for column in line.split():
if column[:7] == '--type=':
resType = column[7:]
elif column[:7] == '--name=':
resName = column[7:].replace('\'', '')
elif column[:11] == '--language=':
resLang = column[11:]
return (resName, resType, resLang)
def extractSubResource(line):
"Extract sub resource from icotool output"
resName = ''
resType = ''
resWidth = ''
resHeight = ''
resDepth = ''
# Split line in fields
for column in line.split():
if column[:8] == '--index=':
resName = column[8:]
elif column[:8] == '--width=':
resWidth = column[8:]
elif column[:9] == '--height=':
resHeight = column[9:]
elif column[:12] == '--bit-depth=':
resDepth = column[12:]
elif column[:6] == '--icon':
resType = _('icon')
elif column[:8] == '--cursor':
resType = _('cursor')
return (resName, resType, resWidth, resHeight, resDepth)
def addFilter(name, patterns=None, mimetypes=None):
"Returns a filter with patterns and mimetypes for gtkFileChoosers"
filter = gtk.FileFilter()
filter.set_name(name)
if patterns:
for pattern in patterns:
filter.add_pattern(pattern)
if mimetypes:
for mimetype in mimetypes:
filter.add_mime_type(mimetype)
return filter
def createColumns():
"Add new columns to the treeview"
newCell = gtk.CellRendererPixbuf()
newCell.set_property('xalign', 1.00)
newColumn = gtk.TreeViewColumn(_('Preview'), newCell, pixbuf=9)
newColumn.set_resizable(True)
tvwResources.append_column(newColumn)
newCell = gtk.CellRendererText()
newColumn = gtk.TreeViewColumn(_('Type'), newCell, text=2)
newColumn.set_resizable(True)
tvwResources.append_column(newColumn)
newCell = gtk.CellRendererText()
newColumn = gtk.TreeViewColumn(_('Name'), newCell, text=3)
newColumn.set_resizable(True)
newColumn.set_expand(True)
tvwResources.append_column(newColumn)
newCell = gtk.CellRendererText()
newCell.set_property('xalign', 0.50)
newColumn = gtk.TreeViewColumn(_('Width'), newCell, text=5)
newColumn.set_resizable(True)
tvwResources.append_column(newColumn)
newCell = gtk.CellRendererText()
newCell.set_property('xalign', 0.50)
newColumn = gtk.TreeViewColumn(_('Height'), newCell, text=6)
newColumn.set_resizable(True)
tvwResources.append_column(newColumn)
newCell = gtk.CellRendererText()
newCell.set_property('xalign', 0.50)
newColumn = gtk.TreeViewColumn(_('Bits'), newCell, text=7)
newColumn.set_resizable(True)
tvwResources.append_column(newColumn)
newCell = gtk.CellRendererText()
newCell.set_property('xalign', 1.00)
newColumn = gtk.TreeViewColumn(_('Size'), newCell, text=8)
newColumn.set_resizable(True)
tvwResources.append_column(newColumn)
newCell = gtk.CellRendererToggle()
newColumn = gtk.TreeViewColumn(_('Extract'), newCell, active=0)
newColumn.set_resizable(False)
newColumn.set_expand(False)
newCell.set_property('activatable', True)
newCell.connect('toggled', on_selected_toggle)
tvwResources.append_column(newColumn)
def on_winMain_delete_event(widget, data=None):
"Close the main Window and gtk main loop"
gtk.main_quit()
return 0
def on_btnRefresh_clicked(widget, data=None):
"Extract resources from the selected filename"
global isLoading
global intTotalResources
global intSelectedResources
btnFilePath.set_sensitive(isLoading)
if not isLoading:
# Start loading
btnRefresh.set_label('gtk-stop')
isLoading = True
else:
# Abort loading
btnRefresh.set_label('gtk-refresh')
isLoading = False
return
resourcesType = {'12': _('cursors'), '14': _('icons')}
intTotalResources = 0
intSelectedResources = 0
# Freeze updates and disconnect model to load faster
tvwResources.freeze_child_notify()
tvwResources.set_model(None)
# Hide save button and show progressbar
btnSaveResources.hide()
progLoading.show()
progLoading.set_fraction(0.0)
# Remove previous files in temporary dir
for f in os.listdir(tempdir):
os.remove(os.path.join(tempdir, f))
# Extract resources list from executable
proc = subprocess.Popen(['wrestool', '--list', btnFilePath.get_filename()],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if stderr and options.verbose:
print 'wrestool --list error: %s' % stderr
# Remove weird characters
stdout = stdout.replace('[', '')
stdout = stdout.replace(']', '')
stdout = stdout.split('\n')
resCount = len(stdout)
resItem = 0
# Clear model and add new resources
modelResources.clear()
for line in stdout:
if not isLoading:
break
resName, resType, resLang = extractResource(line)
# Only cursors and icon groups are well supported by wrestool
if resType in ('12', '14'):
# Extract icon to temporary filename (filename_type_name_lang.ico)
tmpFile = os.path.join(tempdir, '%s_%s_%s_%s.%s' % (
os.path.basename(btnFilePath.get_filename()),
resType, resName, resLang, resType == '12' and 'cur' or 'ico'
))
proc = subprocess.Popen(['wrestool', '-x', '-t', resType, '-n', resName,
'-L', resLang, '-o', tmpFile, btnFilePath.get_filename()],
stderr=subprocess.PIPE)
proc.communicate()
if stderr and options.verbose:
print 'wrestool -x error: %s' % stderr
# Check if the resource was extracted successfully (cannot be sure)
if os.path.isfile(tmpFile):
# Add main resource to the treeview
iter = modelResources.append(None, [
True, int(resType), resourcesType[resType], resName, resLang,
None, None, None, str(os.path.getsize(tmpFile)), None
])
intTotalResources += 1
intSelectedResources += 1
# Extract resources list from group resources
proc = subprocess.Popen(['icotool', '--list', tmpFile],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if stderr and options.verbose:
print 'icotool --list error: %s' % stderr
# Split line in fields
for line in stdout.split('\n'):
if not isLoading:
break
subResource = extractSubResource(line)
if subResource[0]:
# Extract subresources to temporary filename (name_index_WxHxD.png)
tmpFile2 = os.path.join(tempdir, '%s_%s_%sx%sx%s.png' % (
tmpFile[:-4], subResource[0],
subResource[2], subResource[3], subResource[4]))
proc = subprocess.Popen(['icotool', '-x', '-i', subResource[0],
'-w', subResource[2], '-h', subResource[3], '-b', subResource[4],
'-o', tmpFile2, tmpFile], stderr=subprocess.PIPE)
proc.communicate()
if stderr and options.verbose:
print 'icotool -x error: %s' % stderr
# Check if the resource was extracted successfully (cannot be sure)
if os.path.isfile(tmpFile2):
# Add sub-resource to treeview
modelResources.append(iter, [
True, -1, subResource[1], subResource[0], None, subResource[2],
subResource[3], subResource[4], str(os.path.getsize(tmpFile2)),
gtk.gdk.pixbuf_new_from_file(tmpFile2)
])
intTotalResources += 1
intSelectedResources += 1
# Update progressbar
resItem += 1
progLoading.set_fraction(float(resItem) / resCount)
while gtk.events_pending():
gtk.main_iteration()
# Hide progressbar and update treeview
progLoading.hide()
btnSaveResources.show()
btnFilePath.set_sensitive(True)
tvwResources.set_model(modelResources)
tvwResources.thaw_child_notify()
tvwResources.expand_all()
refreshTotals()
# End of loading
isLoading = False
btnRefresh.set_label('gtk-refresh')
def on_btnSaveResources_clicked(widget, data=None):
"Save selected resources"
# Iter the first level
for iter in modelResources:
tmpFile = os.path.join(tempdir, '%s_%s_%s_%s.%s' % (
os.path.basename(btnFilePath.get_filename()),
iter[1], iter[3], iter[4], iter[1] == 12 and 'cur' or 'ico'
))
if iter[0]:
# Copy ico/cur file
shutil.copy(tmpFile, btnDestination.get_filename())
# Iter the children
for iter in iter.iterchildren():
if iter[0]:
# Copy png subicon
shutil.copy(os.path.join(tempdir, '%s_%s_%sx%sx%s.png' % (
tmpFile[:-4], iter[3], iter[5], iter[6], iter[7])),
btnDestination.get_filename())
diag = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO,
gtk.BUTTONS_OK, _('Extraction completed.'))
diag.set_icon_from_file(APP_LOGO)
diag.run()
diag.destroy()
def on_selected_toggle(renderer, path, data=None):
"Select or deselect an item"
global intSelectedResources
modelResources[path][0] = not modelResources[path][0]
intSelectedResources += modelResources[path][0] and 1 or -1
updateTotals()
def on_btnSelect_clicked(widget, data=None):
"Select or deselect items"
global intSelectedResources
intSelectedResources = 0
for iter in modelResources:
# Iter the first level
if widget is btnSelectAll:
iter[0] = True
intSelectedResources += 1
elif widget is btnDeselectAll or widget is btnSelectPNG:
iter[0] = False
# Iter the children
for iter in iter.iterchildren():
if (widget is btnSelectAll) or (widget is btnSelectPNG):
iter[0] = True
intSelectedResources += 1
elif widget is btnDeselectAll:
iter[0] = False
updateTotals()
def on_btnFilePath_file_set(widget, data=None):
"Activates or deactivates Refresh button if file was set"
global intTotalResources
global intSelectedResources
if btnFilePath.get_filename():
set_enabled(btnRefresh, bool(btnFilePath.get_filename()))
modelResources.clear()
intTotalResources = 0
intSelectedResources = 0
refreshTotals()
on_btnRefresh_clicked(None)
def on_btnInfo_clicked(widget, data=None):
"Shows the about dialog"
about = gtk.AboutDialog()
about.set_program_name(APP_TITLE)
about.set_version(APP_VERSION)
about.set_comments(_('A GTK utility to extract cursors, icons and png image '
'previews from MS Windows resource files (like .exe, .dll, .ocx, .cpl).'))
about.set_icon_from_file(APP_LOGO)
about.set_logo(gtk.gdk.pixbuf_new_from_file(APP_LOGO))
about.set_copyright('Copyright 2009 Fabio Castelli')
about.set_translator_credits(readTextFile(getPath('doc','translators')))
about.set_license(readTextFile(getPath('doc','copyright')))
about.set_website_label('gExtractWinIcons')
gtk.about_dialog_set_url_hook(lambda url, data=None: url)
about.set_website('http://code.google.com/p/gextractwinicons/')
about.set_authors([
'Fabio Castelli <muflone@vbsimple.net>',
'http://www.ubuntutrucchi.it'
])
about.run()
about.destroy()
def on_btnFilePath_update_preview(widget, data=None):
"Select first filter if it's not selected (bug?)"
if not btnFilePath.get_filter():
btnFilePath.set_filter(btnFilePath.list_filters()[0])
# Command line options and arguments
parser = OptionParser(usage='usage: %prog [options]')
parser.add_option('-v', '--verbose', action='store_true',
help='show error messages from wrestool and icotool')
parser.add_option('-d', '--destination',
help='set destination folder')
parser.add_option('-f', '--filename',
help='set resources filename')
parser.add_option('-r', '--refresh', action='store_true',
help='automatically refresh resources list if -f specified')
(options, args) = parser.parse_args()
if options.refresh and not options.filename:
parser.error("option -r requires filename specified with -f")
# Signals handler
signals = {
'on_winMain_delete_event': on_winMain_delete_event,
'on_btnRefresh_clicked': on_btnRefresh_clicked,
'on_btnSaveResources_clicked': on_btnSaveResources_clicked,
'on_btnSelect_clicked': on_btnSelect_clicked,
'on_btnFilePath_file_set': on_btnFilePath_file_set,
'on_btnInfo_clicked': on_btnInfo_clicked,
'on_btnFilePath_update_preview': on_btnFilePath_update_preview
}
# Load domain for translation
for module in (gettext, gtk.glade):
module.bindtextdomain(APP_NAME, getPath('locale'))
module.textdomain(APP_NAME)
# Load interfaces
gladeFile = gtk.glade.XML(fname=getPath('data', '%s.glade' % APP_NAME),
domain=APP_NAME)
gladeFile.signal_autoconnect(signals)
gw = gladeFile.get_widget
winMain = gw('winMain')
winMain.set_default_size(600, 400)
winMain.set_icon_from_file(APP_LOGO)
btnFilePath = gw('btnFilePath')
btnDestination = gw('btnDestination')
progLoading = gw('progLoading')
btnRefresh = gw('btnRefresh')
btnSaveResources = gw('btnSaveResources')
btnSelectAll = gw('btnSelectAll')
btnDeselectAll = gw('btnDeselectAll')
btnSelectPNG = gw('btnSelectPNG')
tvwResources = gw('tvwResources')
lblTotals = gw('lblTotals')
# Adapt height of littler widgets to the biggers
progLoading.set_size_request(-1, btnSaveResources.size_request()[1])
btnRefresh.set_size_request(
btnRefresh.size_request()[0]+20, btnRefresh.size_request()[1]
)
btnDestination.set_size_request(-1, btnRefresh.size_request()[1])
# Add filters for select file button
btnFilePath.add_filter(addFilter(_('MS Windows compatible files'),
['*.exe', '*.dll', '*.cpl', '*.ocx', '*.scr'], None))
btnFilePath.add_filter(addFilter(_('All files'), ['*'], None))
# Init variables
strTotalsTemplate = _('%d resources found (%d resources selected)')
intTotalResources = 0
intSelectedResources = 0
isLoading = False
# Set model for treeview
modelResources = gtk.TreeStore(
bool, # Selected
int, # Type
str, # Type name
str, # Name
str, # Language
str, # Width (cannot use int for missing values in first level)
str, # Height
str, # Color depth
str, # Size
gtk.gdk.Pixbuf # Preview
)
createColumns()
tvwResources.set_model(modelResources)
set_enabled = lambda widget, status: widget.set_property('sensitive', status)
# Create temporary directory for icons
tempdir = tempfile.mkdtemp(prefix='%s-' % APP_NAME)
# Set filename if --filename was specified (and the file is accepted)
if options.filename and btnFilePath.select_filename(options.filename):
set_enabled(btnRefresh, True)
# Set destination folder if --destination was specified else set user's home
btnDestination.set_filename(
not options.destination
and os.path.expandvars('$HOME') or options.destination)
# Show main window and start program
winMain.show()
# Automatically refresh list if --refresh was specified
if options.refresh:
while gtk.events_pending():
gtk.main_iteration()
on_btnFilePath_file_set(btnFilePath)
gtk.main()
# Clear temporary files and dir
for f in os.listdir(tempdir):
os.remove(os.path.join(tempdir, f))
os.rmdir(tempdir)