/* Copyright (C) 2009 Mobile Sorcery AB This program is free software; you can redistribute it and/or modify it under the terms of the Eclipse Public License v1.0. 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 Eclipse Public License v1.0 for more details. You should have received a copy of the Eclipse Public License v1.0 along with this program. It is also available at http://www.eclipse.org/legal/epl-v10.html */ package com.mobilesorcery.sdk.core; import java.io.File; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.AbstractMap.SimpleEntry; import java.util.Map.Entry; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; /** * A simple class for handling icon and their injection for Linux * platforms. At the time this was written, the icon-injector tool * in the MoSync source tree did have the required behavior for * use on Linux platforms, thus this class was written. * Note: This class is dependent on the eternal tool ImageMagick. * * @author Ali Mosavian */ public class IconManager { /** * Internal class that is used to keep track of icons * * @author Ali Mosavian */ static class Icon { private int m_width; private int m_height; private File m_path; /** * Constructor * * @param w width * @param h height * @param p Path to icon */ public Icon ( int w, int h, String p ) { m_width = w; m_height= h; m_path = new File( p ); } /** * Score this icon against the requested size. * The algorithm scoring algorithm works as such, * - If the size are equal, then Integer.MAX_VALUE is returned * * - If this icon is larger, a positive score is returned. * The larger it is, the higher the score is. * * - If this icon is smaller (on any side), a negative score is * returned. The smaller it is, the less the score will be. * * @param w Requested width * @param h Requested height * * @return Score, higher score means better fit */ public int getScore ( int w, int h ) { int dw = w-m_width; int dh = h-m_height; int score2 = Math.abs( dw )+Math.abs( dh ); if ( dw < 0 || dh < 0 ) score2 = -score2; return (score2 == 0 ? Integer.MAX_VALUE : score2); } /** * Returns File instance of icon * * @return File object */ public File getFile ( ) { return m_path; } /** * Checks if this is SVG icon * */ public boolean isSVG ( ) { return m_path.getName( ).endsWith( ".svg" ); } } private Map<String, List<Icon>> m_iconMap; private DefaultPackager m_internal; /** * Constructor, will go through the project directory for any * files ending with ".icon" and parse them. * * @param p Used to execute commands * @param b Project base directory * * @throws Exception * @throws CoreException */ public IconManager ( DefaultPackager p, MoSyncProject b ) throws Exception, CoreException { m_internal = p; m_iconMap = new HashMap<String, List<Icon>>( ); File iconXML = b.getIconFile(); if(null != iconXML) loadIconMetaData(iconXML); } /** * Returns wether or not an icon type exists * * @param type Icon type, either "svg" of some bitmap format * such as PNG. * * @return True on success, false otherwise */ public boolean hasIcon ( String type ) { if ( type.equals( "svg" ) == true && m_iconMap.containsKey( "vector" ) == false ) return false; else if ( m_iconMap.containsKey( "bitmap" ) == false ) return false; return true; } /** * Will "inject", that is copy or convert the best fitting * icon to the output file. Conversion is done with the * ImageMagick tool which is assumed to be in %mosync-bin%. * * @param o The file where the icon will be written to. * * @param w The width of the output icon * @param h The height of the output icon * @param type Output type, either "svg" of some bitmap format * such as PNG. * * @return True on success, false otherwise * @throws Exception If file copying fails, for instance. */ public boolean inject ( File o, int w, int h, String type ) throws Exception { Icon ico; char sep = File.separatorChar; // Create a console which can be used for debug messaging to the Eclispe console IProcessConsole console = CoreMoSyncPlugin.getDefault().createConsole(MoSyncBuilder.CONSOLE_ID); // Can't do SVG output without source SVG if ( type.equals( "svg" ) == true ) { console.addMessage("injecting svg icon"); if ( m_iconMap.containsKey( "vector" ) == false ) return false; ico = m_iconMap.get( "vector" ).get( 0 ); if ( ico.getFile( ).exists( ) == false ) return false; Util.copyFile( new NullProgressMonitor( ), ico.getFile( ), o ); } else { console.addMessage("injecting bitmap icon!"); ico = findBestMatch( w, h ); if(null == ico) console.addMessage("no icon retrived!"); // Check if size is the same if ( ico.getScore( w, h ) == Integer.MAX_VALUE ) { console.addMessage("using unconverted icon :" + ico.getFile( ).getAbsolutePath( )); Util.copyFile( new NullProgressMonitor( ), ico.getFile( ), o ); } else { // Convert console.addMessage("converting icon :" + ico.getFile( ).getAbsolutePath( )); String bin = MoSyncTool.getDefault( ).getBinary( "ImageMagick/convert" ).toOSString( ); if ( m_internal.runCommandLineWithRes( bin, ico.getFile( ).getAbsolutePath( ), "-resize", w + "x" + h, o.getAbsolutePath( ) ) != 0 ) return false; } } return true; } /** * This method finds the best matching bitmap icon for the * requested with and height. The resulting icon does not * have to be the same size or even the same format as you * think it is. So make sure you convert it to the right * size and format. * * @param w The requested with * @param h The requested height. * * @return A bitmap icon which is the closest to the requested * resolution. * * @throws Exception If there aren't any icons at all. */ private Icon findBestMatch ( int w, int h ) throws Exception { Icon icon = null; int score = Integer.MIN_VALUE; // SVG always has preference /* * Unfortunately, ImageMagick doesn't seem to have proper * SVG support, so although this would have been ideal, * it can not be used. * if ( m_iconMap.containsKey( "vector" ) ) return m_iconMap.get( "vector" ).get( 0 ); */ if ( m_iconMap.containsKey( "bitmap" ) == false ) throw new Exception( "No icons" ); for ( Icon i : m_iconMap.get( "bitmap" ) ) { if ( i.getScore( w, h ) < score ) continue; icon = i; score = i.getScore( w, h ); } return icon; } /** * Loads icon meta data from XML * Note: It is assumed that the input XML(s) have the following * XML format * * <icon> * <instance src="someicon.svg"/> * <instance src="someicon1.png" size="w1xh1"/> * <instance src="someicon2.bmp" size="w2xh2"/> * . * . * . * <instance src="someiconN.jpg" size="wNxhN"/> * </icon> * * If multiple SVG files are defined, the first one will * always be chosen. For bitmap formats ( non-vector ) * that is, the one that is closest in size to the output * will be chosen for conversion. * * @param f XML file * * @throws Exception Occurs when the XML is badly formated. */ private void loadIconMetaData ( File f ) throws Exception { SAXParser saxParse; XMLHandler iconParser = new XMLHandler( ); SAXParserFactory saxFact = SAXParserFactory.newInstance( ); try { saxParse = saxFact.newSAXParser( ); saxParse.parse( f, iconParser ); } catch ( Exception e ) { throw new Exception( "Failed to parse icon xml", e ); } List<Icon> bitmapList = new LinkedList<Icon>( ); // Go through icons for ( Entry<String, Entry<Integer, Integer>> e : iconParser.getIconList( ) ) { int wdth = e.getValue( ).getKey( ); int hght = e.getValue( ).getValue( ); String path = f.getParent( ) + File.separatorChar + e.getKey( ); Icon icon = new Icon( wdth, hght, path ); if ( icon.isSVG( ) == true ) { List<Icon> l = new LinkedList<Icon>( ); l.add( icon ); m_iconMap.put("svg", l); } else bitmapList.add( icon ); } m_iconMap.put("bitmap", bitmapList); } } /** * SAX event handler for parsing icon meta XML * * @author Ali Mosavian * */ class XMLHandler extends DefaultHandler { Stack<String> m_parseStack; List<Entry<String, Entry<Integer, Integer>>> m_iconList; /** * Constructor * Inits parse stack and other internal data structures. * */ public XMLHandler ( ) { m_parseStack = new Stack<String>( ); m_iconList = new LinkedList<Entry<String, Entry<Integer, Integer>>>( ); } /** * Invoked by SAX parser at the start of an XML tag * * @param uri The Namespace URI, or the empty string if the element * has no Namespace URI or if Namespace processing is not * being performed. * * @param localName The local name (without prefix), or the empty string * if Namespace processing is not being performed. * * @param qName The qualified name (with prefix), or the empty string if * qualified names are not available. * * @param attributes The attributes attached to the element. If there are * no attributes, it shall be an empty Attributes object. * * @exception RuntimeException Occurs if the XML isn't properly formated. */ public void startElement( String uri, String localName, String qName, Attributes atts ) { // New icon ? if ( qName.equals( "instance" ) == true ) { if ( m_parseStack.empty( ) == false && m_parseStack.peek( ).equals( "icon" ) == true ) { int w = 0; int h = 0; String size = ""; String path = ""; Entry<Integer, Integer> iconSize; // Parse attributes for ( int i = 0; i < atts.getLength( ); i++ ) { String n = atts.getQName( i ); if ( n.equals( "size" ) == true ) size = atts.getValue( i ); else if ( n.equals( "src" ) == true ) path = atts.getValue( i ); } // Semantic check if ( path.isEmpty( ) == true ) throw new RuntimeException( "Badly formated icon xml: no src" ); if ( size.isEmpty( ) == true && path.endsWith( ".svg" ) == false ) throw new RuntimeException( "Badly formated icon xml: src, but no size" ); // Parse size if ( size.isEmpty( ) == false ) { if ( size.equalsIgnoreCase( "default" ) == false ) { String[] tmp = size.split( "x" ); if ( tmp.length != 2 ) throw new RuntimeException( "Badly formated icon xml: bad size format" ); try { w = Integer.parseInt( tmp[0] ); h = Integer.parseInt( tmp[1] ); } catch ( NumberFormatException e ) { throw new RuntimeException( "Badly formated icon xml, size attribute", e ); } } } // Add icon to list iconSize = new SimpleEntry<Integer, Integer>( w, h ); m_iconList.add( new SimpleEntry<String, Entry<Integer,Integer>>( path, iconSize ) ); } } m_parseStack.push( qName ); } /** * Invoked by SAX parser at the end of an XML tag. * * @param uri The Namespace URI, or the empty string if the element * has no Namespace URI or if Namespace processing is not * being performed. * * @param localName The local name (without prefix), or the empty string * if Namespace processing is not being performed. * * @param qName The qualified name (with prefix), or the empty string if * qualified names are not available. */ public void endElement ( String uri, String localName, String qName ) { if ( m_parseStack.empty( ) == true ) return; if ( m_parseStack.peek( ).equals( qName ) == true ) m_parseStack.pop( ); } /** * Returns icon list * * @return Every entry is <Path, <Width, Height>> */ public List<Entry<String, Entry<Integer, Integer>>> getIconList ( ) { return m_iconList; } }