#!/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)
