miércoles, 30 de julio de 2008

Aplicaciones Java (VI): Salida de consola

A veces es conveniente poder escribir trazas para saber lo que está haciendo nuestro programa en cada momento, por ejemplo:

10:24:25 - Descargando fichero "342314.png"
10:24:29 - Descargando fichero "342315.png"
10:24:40 - Descargando fichero "342316.png"
...

En aplicaciones de consola (las que ejecutamos por línea de comandos), crear trazas de este tipo es trivial:

System.out.println("Fichero descargado");

Sin embargo, en aplicaciones gráficas no veremos el intérprete de comandos, y por tanto no podremos acceder a la salida de consola.

Una opción es escribir en un fichero de log, para lo que podemos utilizar una librería del estilo de log4j. Sin embargo, un fichero de log es más problemático para aplicaciones que queremos ejecutar desde la web, sin instalar nada, y no está integrado en el interfaz de la aplicación, sino que hay que acceder a él por separado.

Vamos a crear una clase que nos permitirá, con el mismo código, escribr trazas en stdout, o en un componente de tipo JTextArea.

Esta clase tendrá el modificador final, de modo que no será necesario instanciarla. De este modo, desde cualquier punto de nuestro programa, podremos ejecutar:

Console.println("Fichero descargado");

Que como vemos es muy sencillo y limpio.

En nuestra aplicación únicamente tendremos que crear el JTextArea y asignarlo al objeto Console.

JTextArea consoleOutput = new JTextArea(20, 40);
consoleOutput.setEditable(false);
Console.useJTextComponent(consoleOutput);

Si no reasignamos la salida de la consola a un JTextArea, la salida por defecto será stdout.

Esta es la implementación de la clase Console:

package org.dreamcoder.SwingSample;

import javax.swing.text.PlainDocument;
import javax.swing.text.JTextComponent;
import java.io.PrintStream;

public final class Console
{
 private static final long  serialVersionUID = 3;
 
 private static JTextComponent textField;
 private static PrintStream    stream = System.out; // by default we will output to stdout

    private Console()
    {
        throw new AssertionError("I am a static class. Don't instantiate me.");
    }
    
 public static void useJTextComponent(JTextComponent newTextArea)
 {
  textField = newTextArea;
 }

 public static void usePrintStream(PrintStream newStream)
 {
  stream = newStream;
 }

 public static void println(String str)
 {
        if (textField != null)
        {
            PlainDocument doc = (PlainDocument)textField.getDocument();
            
            try {
                doc.insertString(doc.getLength(), str+"\n", null);
            } catch (javax.swing.text.BadLocationException e) {}

            textField.setCaretPosition(doc.getLength());
        }
        else
        {
            stream.println(str);
        }
    }
}

Vemos en la implementación del método println que puede funcionar de 2 modos: con un stream (como System.out o un fichero, y con un componente que contenga texto). Para este último modo, obtenemos el documento (PlainDocument) correspondiente al componente, y trabajaremos con este documento. El documento es la parte del componente que contiene los datos, independientemente de la representación gráfica.

A partir de esta clase, si nuestra aplicación lo requiere podemos crear una variación que nos permita instanciar diferentes objetos de esta clase, cada uno escribiendo en un JTextArea o PrintStream diferente. Por ejemplo, podríamos tener 2 consolas distintas basadas en un JTextArea, y una tercera salida que escriba en stdout o en un fichero, socket, etc.

Material

El código fuente mostrando el uso de estos componentes se puede descargar bajo licencia GPLv3:
Download

Ejecutar ejemplo, como applet o como Java Web Start (recomendado):
Start Applet Launch via Java Web Start

jueves, 24 de julio de 2008

Aplicaciones Java (V): Creación de una interfaz multiidioma

Supongamos que queremos implementar la interfaz de nuestro programa de modo que se pueda elegir el idioma. La opción que presentamos a continuación nos va a permitir implementar este cambio de idioma sin tener que hardcodear cada una de las posibles combinaciones de componentes e idiomas en el código.

La clave la tenemos en utilizar un objeto del tipo Properties. Ésta es una clase de Java que nos permitirá, por medio de su método getProperty, acceder a un fichero de texto que contiene pares propiedad - valor. El nombre de la propiedad y su valor pueden estar separados por dos puntos ":", igual "=", espacios " ", tabuladores, o una combinación de varios de estos símbolos. Veamos a continuación el fichero languages.cfg utilizado en el ejemplo. Utilizaremos este fichero desde un objeto Properties.

languages.ES.Menu.Tools:        Herramientas
languages.ES.Menu.Language:     Idioma
languages.ES.Menu.Help:         Ayuda
languages.ES.Menu.About:        Acerca de
languages.ES.Label.label:       Etiqueta
languages.ES.Button.boton:      Botón
languages.ES.Table.tabla.0:     primera columna
languages.ES.Table.tabla.1:     segunda columna
languages.ES.Table.tabla.2:     tercera columna

languages.EN.Menu.Tools:        Tools
languages.EN.Menu.Language:     Language
languages.EN.Menu.Help:         Help
languages.EN.Menu.About:        About
languages.EN.Label.label:       Label
languages.EN.Button.boton:      Button
languages.EN.Table.tabla.0:     first column
languages.EN.Table.tabla.1:     second column
languages.EN.Table.tabla.2:     third column

Observamos que para cada componente, hemos definido una propiedad para cada idioma, que sólo cambia en el segundo token (ES/EN). Esto nos permitirá implementar una función para traducir la interfaz, tal que le pasemos el idioma, y buscaremos automáticamente el conjunto de propiedades correspondientes al idioma seleccionado.

En primer lugar cargaremos el fichero con las traducciones en el objeto Properties. Intentaremos leer el fichero del filesystem (mediante FileReader), y si no encontramos el fichero, intentaremos leerlo desde dentro del fichero jar (Class.getResourceAsStream).

A continuación leeremos la propiedad correspondiente getProperty("languages."+language+"."+tipo_compomente+"."+nombre_componente), y cambiaremos ese texto en el componente correspondiente.

private void translateInterface(String language)
{
    // read the properties file that will contain the strings for every language
    
    if (languagescfg == null)
    {
        languagescfg = new Properties();

        try {
            // We will try to open the file from a resource, in case it is embedded in a jar file
            InputStream is = this.getClass().getClassLoader().getResourceAsStream("languages.cfg");
            
            if (is != null)
                languagescfg.load(new InputStreamReader(is));
            else languagescfg.load(new FileReader("languages.cfg"));
        } catch (FileNotFoundException e)
        {        
            System.err.println("File or resource \"languages.cfg\" not found");
        } catch (IOException e)
        {
            System.err.println("Error while reading file \"languages.cfg\"");
        }
    }

    // menus
    toolsmenu.setText(languagescfg.getProperty("languages."+language+".Menu.Tools"));
    languagemenu.setText(languagescfg.getProperty("languages."+language+".Menu.Language"));
    helpmenu.setText(languagescfg.getProperty("languages."+language+".Menu.Help"));
    aboutitem.setText(languagescfg.getProperty("languages."+language+".Menu.About"));
    
    // labels
    label.setText(languagescfg.getProperty("languages."+language+".Label.label"));
            
    // buttons
    boton.setText(languagescfg.getProperty("languages."+language+".Button.boton"));
    
    // Headers de tablas
    tabla.getColumnModel().getColumn(0).setHeaderValue(languagescfg.getProperty("languages."+language+".Table.tabla.0"));
    tabla.getColumnModel().getColumn(1).setHeaderValue(languagescfg.getProperty("languages."+language+".Table.tabla.1"));
    tabla.getColumnModel().getColumn(2).setHeaderValue(languagescfg.getProperty("languages."+language+".Table.tabla.2"));
}

Finalmente sólo queda llamar al método translateInterface, tanto al final de la función que genera el interfaz (para asignar inicialmente los valores por defecto), como en la escucha de los eventos que saltarán cuando el usuario seleccione el idioma correspondiente desde el menú Herramientas->Idioma.

public void generaInterfaz()
{
    // creamos los componentes deseados
    // ...
    
    // Translations of the interface
    
    translateInterface("ES");
}

public void actionPerformed(ActionEvent event)
{
    Object source = event.getSource();
    if (source.getClass().getName().equals("javax.swing.JRadioButtonMenuItem"))
    {
        if (source == languageitemES)
        {
            translateInterface("ES");
        }
        else if (source == languageitemEN)
        {
            translateInterface("EN");
        }
    }
}

Material

El código fuente mostrando el uso de estos componentes se puede descargar bajo licencia GPLv3:
Download

Ejecutar ejemplo, como applet o como Java Web Start (recomendado):
Start Applet Launch via Java Web Start

Aplicaciones Java (IV): Swing: tablas y diálogos

Más formas para mostrar información: tablas y diálogos.

Tablas (JTable, DefaultTableModel)

La clase JTable nos permitirá crear matrices de celdas donde podremos insertar texto, botones, o cualquier otro componente.

final String[] columnNames = {"number", "string 1", "string 2"};
final Object[][] data = {};

JTable tabla = new JTable(new DefaultTableModel(data, columnNames));
root.add(new JScrollPane(tabla), BorderLayout.CENTER);

En el ejemplo anterior hemos utilizado el modelo de tabla "DefaultTableModel". Si queremos personalizar el comportamiento de la tabla, podemos crear una clase que herede de DefaultTableModel, sobreescribiendo algunos de sus métodos.

En el siguiente ejemplo utilizamos este método para hacer que trate la primera columna como objetos de tipo Integer, y las demás como String, frente al comportamiento por defecto que es tratar todas las columnas como objetos de tipo Object. Esto afecta por ejemplo a la ordenación de las columnas cuando hacemos una llamada a setAutoCreateRowSorter.

final String[] columnNames = {"number", "string 1", "string 2"};
final Object[][] data = {};

final class WordListTableModel extends DefaultTableModel
{
 private static final long serialVersionUID = 4;
 
    public WordListTableModel(Object[][] data, Object[] columnNames) {
        super(data, columnNames);
    }

    public Class getColumnClass(int columnIndex) {
     if (columnIndex == 0) return Integer.class;
     else return String.class;
    }
}

tabla = new JTable(new WordListTableModel(data, columnNames));
tabla.setAutoCreateRowSorter(true);
root.add(new JScrollPane(tabla), BorderLayout.CENTER);

Para añadir datos a una tabla podemos utilizar el método addRow:

Object[] row = {Integer.valueOf(123), "any value", "botón pulsado"};
((DefaultTableModel)tabla.getModel()).addRow(row);

Diálogos (JDialog)

Los diálogos nos permitirán mostrar ventanas emergentes con información u opciones adicionales. Ejemplos sencillos de diálogos serían:

  • Diálogos de "Acerca de" con información sobre el programa, página web, autor, etc.
  • Un diálogo emergente que aparezca cuando ejecutamos una operación larga, y que incluya una barra de progreso y un botón para cancelar la operación.

Crearemos diálogos por medio de la clase JDialog. También hay otras formas de crearlos, como utilizando la clase JOptionPane.

Hay un problema a la hora de crear un diálogo desde un applet. El objeto raiz es de la clase JApplet, que no desciende de la clase Frame, necesario para poder crear un objeto JDialog. Para solucionarlo, utilizaremos el método getFrameForComponent de la clase JOptionPane, que nos permitirá obtener el objeto del tipo Frame del que es nieto el applet. En el caso de una aplicación, el objeto root ya es un Frame, con lo que getFrameForComponent devolverá el mismo objeto, de modo que podemos utilizar el mismo código para la ejecución como applet y como aplicación:

JDialog dialog = new JDialog(JOptionPane.getFrameForComponent(root), "botón pulsado!!!", Dialog.ModalityType.APPLICATION_MODAL);
dialog.add(new JLabel("line correctly added"));
dialog.pack();
dialog.setVisible(true);

Material

El código fuente mostrando el uso de estos componentes se puede descargar bajo licencia GPLv3:
Download

Ejecutar ejemplo, como applet o como Java Web Start (recomendado):
Start Applet Launch via Java Web Start

miércoles, 23 de julio de 2008

Aplicaciones Java (III): Swing: botones, menús y eventos

Seguimos con los artículos sobre Swing. Pasamos a comentar algunos controles relacionados con la interactividad de nuestro programa: botones y menús, y eventos asociados.

Botones (JButton)

Por medio de la clase JButton crearemos botones a los que asociaremos determinadas acciones.

JButton botonsinusar = new JButton("boton sin usar");
root.add(botonsinusar, BorderLayout.EAST);

Es posible añadir imágenes a los botones. Para ello, utilizaremos el constructor de JButton que permite pasar una imagen como parámetro. Para la imagen del botón, podemos proporcionar un fichero gráfico propio que almacenaremos dentro de nuestro fichero jar, o gracias al método UIManager.getIcon, utilizar una imagen proporcionada por los componentes estándar de Java. En esta página podremos encontrar un listado con algunos de los gráficos incluidos en componentes estándar. Alguien conoce algún listado más exahustivo?

JButton boton = new JButton("texto boton", UIManager.getIcon("FileView.hardDriveIcon"));
root.add(boton, BorderLayout.SOUTH)

Menús (JMenuBar, JMenu, JMenuItem)

Por medio de las clases JMenuBar, JMenu, y JMenuItem/JRadioButtonMenuItem/JCheckBoxMenuItem, podremos fácilmente generar menús para nuestro programa con todas las características que podemos imaginar:

  • Atajos de teclado
  • Iconos
  • Submenús (menú que se abre cuando pones el ratón encima de una opción de otro menú)
  • Botones de radio, para seleccionar una entre múltiples opciones
  • Checkbox, para activar o desactivar una opción

En primer lugar crearemos un objeto del tipo JMenuBar, que es la barra de menú que colocaremos en el panel como un componente más (utilizando el layout correspondiente).

JMenuBar menubar = new JMenuBar();
root.add(menubar, BorderLayout.NORTH);

A este JMenuBar le añadiremos un JMenu, que es cada uno de los menús de una barra de menú (Archivo, Herramientas, Ayuda...).

JMenu toolsmenu = new JMenu("Tools");
menubar.add(toolsmenu);

Cada uno de estos menús puede tener una serie de elementos (JMenuItem, JRadioButtonMenuItem, JCheckBoxMenuItem) o contener menús anidados (un JMenu dentro de un JMenu).

JMenu languagemenu  = new JMenu("Language");
toolsmenu.add(languagemenu);

JMenuItem languageitemEN = new JRadioButtonMenuItem("English");
languagemenu.add(languageitemEN);

JMenuItem languageitemES = new JRadioButtonMenuItem("Español");
languagemenu.add(languageitemES);

JMenuItem aboutitem = new JMenuItem("Exit");
toolsmenu.add(aboutitem);

En el caso de los botones de radio, podremos definir un grupo de botones entre los que sólo será posible seleccionar una opción, utilizando la clase ButtonGroup.

ButtonGroup languagegroup = new ButtonGroup();
languagegroup.add(languageitemEN);
languagegroup.add(languageitemES);
languageitemES.setSelected(true);

Eventos (ActionListener)

Para escuchar los eventos, como los generados por los botones y menús, tendremos que crear una clase que implemente el interfaz ActionListener, creando el método actionPerformed, definido como abstracto. Este método será invocado cada vez que se produzca un evento de los que hemos seleccionado que queremos escuchar. En nuestro caso, la pulsación de botones y menús.

Para añadir la capacidad de escuchar eventos a nuestra clase deberemos añadir lo siguiente:

Para cada componente cuyos eventos queramos escuchar le diremos que será el objeto de la clase actual (this) quien escuchará ese evento. También se puede crear una clase independiente para escuchar estos eventos.

boton.addActionListener(this);
languageitemEN.addActionListener(this);
languageitemES.addActionListener(this);
aboutitem.addActionListener(this);

Utilizando el evento (de la clase clase ActionEvent) pasado como parámetro, podremos distinguir de qué tipo de evento se trata (source.getClass().getName()), identificar el componente concreto que generó el evento event.getSource(), y otra información (getActionCommand()).

Para cada clase que queramos que escuche eventos (en nuestro caso sólo en SwingSampleBase):

public class SwingSampleBase implements ActionListener
{    
    // constructor y otros métodos
    // [...]

    public void actionPerformed(ActionEvent event)
    {
        Object source = event.getSource();
        if (source.getClass().getName().equals("javax.swing.JMenuItem") ||
            source.getClass().getName().equals("javax.swing.JRadioButtonMenuItem"))
        {
            if (source == aboutitem)
            {
                lCenter.setText("selected About item");
            }
            else if (source == languageitemES)
            {
                lCenter.setText("selected Spanish language");
            }
            else if (source == languageitemEN)
            {
                lCenter.setText("selected English language");
            }
        }
        else // JButton
        {
            if (source == boton)
            {
                lCenter.setText("botón pulsado");
            }
        }
    }
}

Material

El código fuente mostrando el uso de estos componentes se puede descargar bajo licencia GPLv3:
Download

Ejecutar ejemplo, como applet o como Java Web Start (recomendado):
Start Applet Launch via Java Web Start

Aplicaciones Java (II): Swing: componentes básicos

En este post vamos a continuar comentando como crear una aplicacion Java con Swing. Concretamente, vamos a mostrar como utilizar varios de los componentes de Swing.

Paneles (JPanel)

Los paneles son los objetos donde colocaremos los componentes que creemos. Son el canvas donde posteriormente pintaremos. Crearemos los paneles normales utilizando la clase JPanel:

JPanel izquierda = new JPanel();
izquierda.add(new JLabel("etiqueta"));

Etiquetas (JLabel)

Probablemente el control más básico es la etiqueta (JLabel). Se trata de un recuadro de texto con el contenido que queramos.

root.add(new JLabel("Hola mundo!"));

Layouts (BorderLayout, BoxLayout, GridLayout)

El punto mas lioso de Swing, especialmente al principio, es la distribucion de los controles en la ventana. Para ello utilizaremos unos objetos llamados layouts. Estos objetos nos permitirán elegir dónde queremos ubicar cada componente.

Hay diferentes tipos de layouts. Algunos de los más básicos son:

BorderLayout

Divide el panel actual en 5 secciones (norte, sur, este, oeste, centro). Intentará dar más espacio a la sección central, con lo que es muy útil cuando tenemos por ejemplo un componente de texto que nos interesa mucho, y una serie de botones que queremos ubicar en la parte de arriba del panel.

root.setLayout(new BorderLayout());
root.add(new JLabel("este"), BorderLayout.EAST);

BoxLayout

Este layout ubicará los componentes uno al lado del otro en un mismo eje, en horizontal o vertical.

JPanel izquierda = new JPanel();
izquierda.setLayout(new BoxLayout(izquierda, BoxLayout.Y_AXIS));

izquierda.add(new JLabel("arriba"));
izquierda.add(new JLabel("centro"));
izquierda.add(new JLabel("abajo"));

GridLayout

Este layout divide la superficie en n·m celdas de igual tamaño.

JPanel gridpanel = new JPanel(new GridLayout(2, 2));
root.add(gridpanel, BorderLayout.SOUTH);
  
gridpanel.add(new JLabel("arriba izquierda"));
gridpanel.add(new JLabel("arriba derecha"));
gridpanel.add(new JLabel("abajo izquierda"));
gridpanel.add(new JLabel("abajo derecha"));

Otros layouts

Hay muchos otros layouts que nos permitirán distribuir de maneras más flexibles nuestros componentes. Se pueden consultar en la ayuda de Java al respecto.

Paneles con pestañas (JTabbedPane)

Un tipo especial de panel nos permitirá agregar varios paneles en el mismo espacio físico, que seleccionaremos por medio de pestañas (tabs).

JTabbedPane tabbedPane = new JTabbedPane();
root.add(tabbedPane, BorderLayout.CENTER);

JPanel tab1 = new JPanel(new BorderLayout());
tabbedPane.addTab("tab 1", tab1);

JPanel tab2 = new JPanel(new BorderLayout());
tabbedPane.addTab("tab 2", tab2);

Campos de texto (JTextArea) y scrolling (JScrollPane)

Utilizaremos el componente JTextArea para permitir al usuario introducir y modificar texto.

JTextArea text1 = new JTextArea(20, 30);
tab1.add(text1);
text1.append("Escribimos algo de texto\nQue aparecerá por defecto.");

Si además añadimos un objeto del tipo JScrollPane, cuando haya más texto que el espacio disponible aparecerá automáticamente una barra de desplazamiento.

JTextArea text2 = new JTextArea(20, 30);
tab2.add(new JScrollPane(text2));
text2.append("Este campo de texto estará oculto en la segunda pestaña, " +
      "y tendrá una barra de scroll.");

Fillers (Filler)

Éste es un componente puramente estético, que sirve para dejar espacio entre dos componentes.

tab3.add(new Filler(new Dimension(10, 10), new Dimension(10, 10), new Dimension(10, 10)));

Selección de un valor en una lista (JComboBox)

Para seleccionar un valor entre una lista predefinida, utilizaremos un componente de tipo JComboBox:

String[] colores = {"azul", "blanco", "rojo", "negro"};
root.add(new JComboBox(colores), BorderLayout.NORTH);

Material

El código fuente mostrando el uso de estos componentes se puede descargar bajo licencia GPLv3:
Download

Y gracias a las tecnologías Java Web Start y Applets comentadas en el artículo anterior, este ejemplo se puede ejecutar directamente sin instalar nada, sólamente pulsando uno de los siguientes botones:
Start Applet Launch via Java Web Start

martes, 22 de julio de 2008

Aplicaciones Java (I): Applets y Java Web Start

Aplicaciones Java

Vamos a comentar una forma muy sencilla de desarrollar aplicaciones multiplataforma que se pueden ejecutar sin necesidad de instalar nada.

Para ello utilizaremos un conjunto de clases desarrolladas para programar interfaces: Swing. Gracias a Swing podremos crear ventanas, botones, campos de texto, selectores de fichero, y cualquier control que necesitemos para nuestra interfaz

Aplicaciones de escritorio, Aplicaciones Java Web Start y Applets

Tenemos 3 formas de ejecutar una aplicación desarrollada en Java, en función de cómo haya sido desarrollada.

Aplicaciones de escritorio

En este caso nos descargamos la aplicación a nuestra máquina local, generalmente en un fichero ".jar", o un zip conteniendo un fichero ".jar". Ejecutaremos la aplicación por medio del binario "java":
java -cp FICHERO_JAR NOMBRE_CLASE
java -cp SwingSample.jar org.dreamcoder.SwingSample.SwingSampleApp

También se puede ejecutar por medio de un script (unix shell script ".sh", DOS batch ".bat" o Windows command ".cmd", VBScript, etc), que lance el comando anterior.

Si el fichero jar se ha creado con la entrada correspondiente en el fichero META-INF/MANIFEST.MF, será posible ejecutar la aplicación directamente, pulsando el botón derecho y "Open with Sun Java * Runtime", o mediante la línea de comandos:
java -jar SwingSample.jar

Entrada del fichero MANIFEST.MF:
Main-Class: CLASE A INVOCAR QUE CONTIENE LA FUNCION MAIN
Main-Class: org.dreamcoder.SwingSample.SwingSampleApp

Applets

Con unos pequeños cambios en el código, y unas limitaciones por motivos de seguridad, podemos convertir nuestra aplicación de escritorio en un Applet. Esto quiere decir que la aplicación se ejecutará dentro de un navegador web, directamente desde Internet, sin instalar nada. Es muy cómodo para el usuario, pero puede haber diversos problemas entre el applet y el navegador web.

Para insertar un applet en nuestra página web deberemos incluir un código similar al siguiente:

<applet code="org.dreamcoder.SwingSample.SwingSampleApplet.class" width="100%" height="100%" archive="SwingSample.jar">Java Applet Support Required</applet>

Ejecutar aplicación como applet: Start Applet

Java Web Start

Éste es un nuevo modo de iniciar aplicaciones que se introdujo con Java 1.2 como un complemento que debías descargar por separado, y a partir de la versión 1.4 ya viene de serie con la instalación base.

La idea es similar a los applets, dado que desde una página web podremos directamente ejecutar nuestro programa. La principal diferencia consiste en que nuestro programa se ejecutará como un proceso independiente del navegador, aislándonos así de muchos problemas. A nuestra máquina se descargará un fichero jnlp, que es un pequeño fichero de texto que indica cómo ejecutar la aplicación.

Con este método se pueden ejecutar aplicaciones que hayan sido desarrolladas como aplicaciones de escritorio tanto como applets. Éste es el método actualmente recomendado por Sun.

Vemos aquí un ejemplo de un fichero jnlp que nos permitirá ejecutar una aplicación Java desde la web, utilizando Java Web Start:

<?xml version="1.0" encoding="utf-8"?>

<jnlp codebase="http://nightearth.com/dreamcoder/SwingSample/SwingSample_initial" href="SwingSample.jnlp">
 <information>
  <title>Swing sample JWS application</title>
  <vendor>dreamcoder.org</vendor>
  <description>Small application that doesn't do anything, just shows how to implement a multiplatform application</description>
  <homepage href="http://www.dreamcoder.org"/>
  <description kind="short">Swing sample JWS application</description>
  <offline-allowed/>
 </information>
 <resources>
  <jar href="SwingSample.jar" />
  <j2se version="1.4+" href="http://java.sun.com/products/autodl/j2se" />
 </resources>
 <application-desc main-class="org.dreamcoder.SwingSample.SwingSampleApp"/>
</jnlp>

Ejecutar aplicación vía Java Web Start: Launch via Java Web Start

Para ejecutar un applet via JWS sustituiremos el elemento application-desc por:

<applet-desc name="SwingSample" main-class="org.dreamcoder.SwingSample.SwingSampleApplet" width="768" height="576"></applet-desc>

Punto de entrada (JFrame, JApplet)

Dependiendo de si queremos desarrollar una aplicación o un applet, deberemos hacer uso de la clase JFrame o JApplet. En cuanto a la implementación, no hay muchas más diferencias. Para nuestros ejemplos, vamos a crear un wrapper de cada tipo, con lo que nuestra aplicación podrá ser ejecutada como applet, como aplición offline, o como aplicación online via Java Web Start, utilizando el mismo fichero jar.

SwingSampleApp.java

Utilizado para la ejecución como aplicación offline y Java Web Start.

package org.dreamcoder.SwingSample;

import javax.swing.JFrame;

public class SwingSampleApp
{
 public static void main(String[] args)
 {
  JFrame frame = new JFrame();
  new SwingSampleBase(frame);
  frame.pack();
 }
}

SwingSampleApplet.java

Utilizado para la ejecución como applet.

package org.dreamcoder.SwingSample;

import javax.swing.JApplet;

public class SwingSampleApplet extends JApplet
{
 private static final long serialVersionUID = 1;
 SwingSampleBase app;
 
 public void init()
 {
  if (app == null)
   app = new SwingSampleBase(this);
 }
}

SwingSampleBase.java

fichero principal de la aplicación, que será utilizado tanto desde el applet como desde la aplicación.

package org.dreamcoder.SwingSample;

import java.awt.Container;
import javax.swing.JLabel;

public class SwingSampleBase
{ 
 // Main element
 Container root;
 
 // Interface
 JLabel     lHolaMundo;

 public SwingSampleBase(Container providedRoot)
 {
  root = providedRoot;
  generaInterfaz();

  root.setVisible(true);
 }

 public void generaInterfaz()
 {
  root.add(new JLabel("Hola mundo!"));
 }
}

Descargar el código: Download

Esto es sólo el esqueleto, donde probamos los métodos para desarrollar y ejecutar una aplicación. En próximos posts le añadiremos contenido a la aplicacion.

Podríais comentar qué tal os funciona la aplicación, cuando intentáis ejecutarla como Applet, vía Java Web Start, o ejecutándola localmente, una vez descargada al disco duro? Así como Sistema Operativo, versión de la JVM (máquina virtual java) y navegador web. Gracias!!!

martes, 8 de julio de 2008

WordFrequency (I): Obtener un listado de las palabras más frecuentes en un conjunto de webs

Introducción

Vamos a desarrollar paso a paso un pequeño programa cuya finalidad es extraer la lista de las palabras más usadas en un listado de páginas web.

La idea es extraer este listado, para enfocarnos a la hora de estudiar vocabulario de un nuevo idioma. O incluso sólo por curiosidad: lo podemos utilizar con un texto nuestro, a ver si repetimos una palabra demasiadas veces.

El lenguaje elegido es java versión 6.0, dado su carácter multiplataforma y su capacidad de ser ejecutado desde una página web como un applet, con lo que no es necesario descargar ni instalar nada para ejecutarlo.

El código fuente estará disponible con licencia GPL, lo que quiere decir que puedes utilizar este código donde quieras y como quieras, siempre que mantengas la referencia al autor original, y el programa que hagas con este código sea también GPL (también distribuyas el código fuente).

Para el desarrollo estoy utilizando el JDK de Sun, y el editor Eclipse, muy cómodo y potente. Todo software libre o gratuíto.

En lugar de mostrar todo el código fuente, vamos a mostrar sólo las partes más interesantes del programa: para cada funcionalidad principal, cómo se ha implementado.

Descargar una página web via http

Por medio de un objeto de la clase java.net.URLConnection obtendremos un stream que podremos utilizar para leer datos de forma estándar, por ejemplo llamando a br.readLine()

URL url = new URL(urlstring);
URLConnection connection = url.openConnection();
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));

Parsear un documento html

Por medio de la clase javax.swing.text.html.HTMLEditorKit parsearemos el documento html en un objeto de tipo javax.swing.text.html.HTMLDocument, que nos permitirá navegar por sus elementos. En este caso seleccionaremos los elementos de tipo HTML.Tag.CONTENT, que es un alias que representa cualquier elemento html que contenga texto. Otros elementos interesantes pueden ser HTML.Tag.A, HTML.Tag.IMG, HTML.Tag.H1...

HTMLEditorKit htmlKit = new HTMLEditorKit();
HTMLDocument htmlDoc = (HTMLDocument) htmlKit.createDefaultDocument();
HTMLEditorKit.Parser parser = new ParserDelegator();
HTMLEditorKit.ParserCallback callback = htmlDoc.getReader(0);
parser.parse(br, callback, true);

// Iterate over all the text tags and insert an entry into the hash map or increase its number of occurrences 

for (HTMLDocument.Iterator iterator = htmlDoc.getIterator(HTML.Tag.CONTENT); iterator.isValid(); iterator.next())
{
    int startOffset = iterator.getStartOffset();
    int endOffset = iterator.getEndOffset();
    int length = endOffset - startOffset;
    String text = htmlDoc.getText(startOffset, length).trim();
}

Con este código, para cada elemento encontrado, habremos almacenado en la variable text el texto que contiene.

Esta clase HTMLDocument también permite acceder a los atributos de cada elemento, como el atributo src de un elemento A. Esto puede ser particularmente útil si queremos por ejemplo extraer las páginas web enlazadas desde una página web dada, o las imágenes que contiene:

for (HTMLDocument.Iterator iterator = htmlDoc.getIterator(HTML.Tag.A); iterator.isValid(); iterator.next())
{
    Object href = iterator.getAttributes().getAttribute( HTML.Attribute.HREF );
 
    if (href != null) // the 'A' element contains the 'href' attribute
    {
        System.out.println("Link found: \"" + href.toString() + "\"");
    }
}

Calcular la frecuencia de aparición de cada palabra

Creamos un objeto de tipo HashMap, que contiene pares "llave-valor", y cada llave es única; esto es, si hacemos hash.put("a", valor) y "a" ya existe, estamos modificando la anterior entrada de "a". La búsqueda (hash.get()) en esta estructura es muy rápida.

HashMap hash = new HashMap();

Para cada palabra encontrada, incrementaremos su valor asociado, que nos indicará el número de veces que esta palabra ha aparecido en la entrada.

// we insert or update the entry in the hashmap by 1
Integer in = (Integer)hash.get(palabra);
if (in == null) in = Integer.valueOf(1);
else in = Integer.valueOf(in.intValue() + 1);

hash.put(palabra, in);

Una vez hemos recopilado la frecuencia de aparición de cada palabra en esta estructura, podemos, por ejemplo, ordenar estos datos, insertándolos en una colección de tipo java.util.TreeSet y su especialmente útil iterador descendingIterator, que recorre el árbol en orden inverso. También utilizamos esta ordenación para seleccionar los 'n' elementos con valor más alto

// generate an ordered set with the most frequent words (at most maxRecords)
TreeSet orderedSet = new TreeSet();
 
Set s = hash.entrySet();
iter = s.iterator();
while (iter.hasNext())
{
    Map.Entry act = (Map.Entry)iter.next();
    orderedSet.add(new DataRecord((Integer)act.getValue(), (String)act.getKey()));
}
 
iter = orderedSet.descendingIterator();
DataRecord min = null;
i = maxRecords;
while ( iter.hasNext() && (i-- > 0) )
{
    min = (DataRecord)iter.next();
}
if (iter.hasNext())
    min = (DataRecord)iter.next();
 
return orderedSet.descendingSet().headSet(min, true);

Y ya sólo nos queda mostrar o almacenar los datos obtenidos.

Material

La implementación del programa comentado en este artículo se puede descargar bajo licencia GPLv3. El código comentado se encuentra dentro de la clase WordFrequencyCalculator.

Download

Y eso es todo por el momento. Cualquier duda o sugerencia que tengáis, dejadla en los comentarios. Y si realizáis cualquier mejora interesante al código, u os es útil de cualquier otra forma, por favor comentádmelo, por curiosidad.

miércoles, 2 de julio de 2008

Análisis del formato PNG

Este es un artículo que escribí hace años para una asignatura de la universidad, y más tarde colgué de una web que ya no existe. así que aprovecho para rescatarlo.

Introducción.

El formato PNG (Portable Network Graphics) es un formato gráfico que usa compresión sin pérdidas (loseless compression). Es el formato actualmente recomendado por la organización W3C (World Wide Web Consortium) para imágenes sin pérdida de calidad.

Se trata de un sustituto al formato GIF (CompuServe's Graphics Interchange Format), que al contrario que éste, está libre de patentes, con lo que puede usarse libremente. Usa una compresión mejor, gracias al uso de las técnicas LZ77 y Huffman y a los filtros sin pérdida de que dispone. Permite imágenes en escala de grises, paleta y truecolor, con y sin canal alfa, permitiendo 256 niveles de transparencias por pixel, a profundidades de bits de 1 a 16 bits por componente (hasta 48 bits por pixel).

Está diseñado para ser utilizado en el Web, con lo que permite su visualización progresiva (es streamable). En especial se beneficia en este apartado del método de entrelazado Adam7, que permite una mejor apreciación de la imagen mucho antes que en el entrelazado del formato GIF.

También tiene control de errores gracias a los códigos CRC-32 y Adler-32 que almacena.

Puede almacenar el gamma y datos de cromaticidad para mejorar la visualización en distintas plataformas (Mac, PC,…), con lo que obtenemos una completa independencia del hardware, al contrario que otros formatos en los que dependiendo de la máquina puede verse más claro o más oscura la imagen, etc.

Especificación del formato.

El formato PNG está compuesto por una cabecera seguido por una serie de chunks o trozos. Cada uno de estos chunks está compuesto a su vez por un entero de 4 bytes que indica la longitud en bytes del chunk, un identificador del chunk de también 4 bytes, el cuerpo del chunk y a continuación un CRC para comprobar la existencia de errores.

Esta estructura permite extender el formato sin afectar la compatibilidad, ya que los chunks principales se mantienen fijos, mientras que son los opcionales o ancillary chunks los que posibilitan las extensiones al formato. En caso de que un descompresor no reconozca o no tenga implementados alguno de los chunks opcionales, basta con saltar el chunk y seguir con el resto de chunks del fichero.

Los chunks principales son:

  • IHDR Image header. Es el primero que debe aparecer en la imagen. Almacena información tal como el ancho y alto de la imagen, su tipo de color, si está entrelazada, etc.
  • PLTE Palette. Si la imagen usa color indexado, debe aparecer este chunk donde indica la paleta a utilizar.
  • IDAT Image data. Aqui se almacena el grueso de la imagen, comprimido usando el algoritmo Deflate (RFC 1951).
  • IEND Image trailer. Indica el final del stream.
  • Además también encontramos otros chunks opcionales, como:

  • tRNS Transparency
  • gAMA Image gamma
  • cHRM Primary chromaticities
  • sRGB Standard RGB color space
  • iCCP Embedded ICC profile
  • tEXt Textual data
  • zTXt Compressed textual data
  • iTXt International textual data
  • bKGD Background color
  • pHYs Physical pixel dimensions
  • sBIT Significant bits
  • sPLT Suggested palette
  • hIST Palette histogram
  • tIME Image last-modification time
  • Estos chunks por ser opcionales pueden no estar en un fichero, además, un descodificador puede ignorarlos sin que esto suponga un grave problema.

    Filtros.

    El formato PNG permite filtrar la imagen antes de pasarla a la etapa de compresión para así obtener mejores resultados. Se puede elegir el filtro que se va a usar en cada scanline, de modo que optimicemos en la medida de lo posible el tamaño final de la imagen comprimida. Dispone de cinco tipos básicos de filtrado sin pérdida de información:

  • Tipo 0: None. La scanline se transmite sin modificar.
  • Tipo 1: Sub. Se computa la diferencia entre la componente actual y la misma componente del pixel anterior.
  • Sub(x) = Raw(x) - Raw(x-bpp)
  • Tipo 2: Up. Se computa la diferencia entre la componente actual y la misma componente del mismo pixel de la anterior scanline.
  • Up(x) = Raw(x) - Prior(x)
  • Tipo 3: Average. Se calcula la media entre el valor de la misma componente del pixel anterior y la misma componente del mismo pixel de la anterior scanline, redondeando siempre hacia abajo. El valor que almacenamos es la diferencia respecto a este valor.
  • Average(x) = Raw(x) - floor((Raw(x-bpp)+Prior(x))/2)
  • Tipo 4: Paeth. Este filtro calcula una función matemática que depende del valor de la misma componente del pixel superior, del pixel situado a la izquierda y del arriba a la izquierda. Una vez calculado este valor usa como predicción el pixel de estos tres que más se acerque al valor calculado.
  • Paeth(x) = Raw(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x-bpp))

    Para ampliar información puede consultarse la especificación del formato PNG, en la RFC 2083.

    Método de compresión utilizado: Deflate/Inflate.

    El método de compresión usado en el formato PNG es un derivado del LZ77, usado en zip, gzip, pkzip, etc.

    Los datos se almacenan en el formato zlib (especificado en la RFC 1950). Un bloque zlib está compuesto por:

  • 2 bytes de cabecera, donde entre otras cosas se guarda el método de compresión utilizado, en el caso del formato PNG, al igual que sucede con el gzip, sólo es válido el método 8 (Deflate).
  • Los datos comprimidos.
  • Un código de detección de errores Adler-32.
  • El método de compresión Deflate está definido en la RFC 1951.

    Una secuencia de datos comprimidos está dividido en una serie de bloques. Cada bloque es comprimido usando una combinación de LZ77 y códigos de Huffman. Los árboles de Huffman para cada bloques son independientes, mientras que el algoritmo LZ77 podría referenciar una cadena que se encuentre en un bloque anterior.

    Las lecturas se realizan bit a bit, en orden ascendente dentro de cada byte, que se lee también ascendentemente. En los códigos de Huffman el primer bit leido es el de mayor peso, y luego continúa descendentemente. Ej:

    byte actual: 11011101 sería leido como:
    10111 (si se trata de un código Huffman de 5 bits)

    El resto de lecturas se interpretan en orden inverso, es decir, el primer bit que leemos es el de menor peso. Ej:

    byte actual: 11011101 sería leido como:
    11011 (si se trata de un código Huffman de 5 bits)

    Uso de códigos de Huffman en Deflate.

    Vamos a definir el uso de los códigos de Huffman para el algoritmo Deflate. Dados dos códigos de igual longitud, es anterior el menor en orden lexicográfico. Así, en el código:

    A 10
    B 0
    C 110
    D 111

    el primer elemento sería el B, seguido del A, C y D.

    Teniendo en cuenta este orden, va a ser posible reconstruir los códigos de Huffman dadas las longitudes respectivas de cada código. Así, el código anterior quedaría representado por la secuencia (2, 1, 3, 3). El algoritmo para reconstruir los códigos de Huffman a partir de sus longitudes de código es el siguiente:

    1) Contar el número de códigos para cada longitud. Sea bl_count[N] el número de códigos de longitud N, N >= 1.

    2) Encuentra el valor numérico del código más pequeño para cada longitud de código:

        code = 0;
     bl_count[0] = 0;
     for (bits = 1; bits <= MAX_BITS; bits++) {
        code = (code + bl_count[bits-1]) << 1;
        next_code[bits] = code;
    }

    3) Asigna valores numéricos a todos los códigos, usando valores consecutivos para todos los códigos de la misma longitud con los valores base determinados en el punto 2. A los códigos que no se usan (que tienen una longitud de 0 bits) no se les debe asignar un valor.

        for (n = 0;  n <= max_code; n++) {
        len = tree[n].Len;
        if (len != 0) {
            tree[n].Code = next_code[len];
            next_code[len]++;
        }
    }

    Formato de bloque.

    En primer lugar encontramos la cabecera del bloque. El primer bit indica si se trata del último bloque del campo zlib, mientras que los dos bits que hay a continuación indican el tipo de compresión que usa el bloque, donde:

    00 --> Sin compresión
    01 --> Comprimido con códigos de Huffman estáticos.
    10 --> Comprimido con códigos de Huffman dinámicos.
    11 --> Reservado (error).

    Bloques sin compresión.

    Tienen una sub-cabecera de 4 bytes, donde:

    LEN (2 bytes): indica el número de bytes de datos en el campo.
    NLEN (2 bytes): complemento a 1 de LEN.

    Bloques con compresión.

    Estos bloques se definen por secuencias de literales y pares <longitud, distancia hacia atrás>. Esto se representa mediante un alfabeto para literales/longitudes y otro para distancias. El alfabeto de literales/longitudes se define como:

    0-255 literales
    256 fin de bloque
    257-285 códigos de longitud (en conjunción con bits adicionales)

    El alfabeto de longitudes con los bits adicionales para cada elemento se muestran en la siguiente tabla:

    Código Bits Extra Longitud Código Bits Extra Longitud Código Bits Extra Longitud
    257 0 3 267 1 15,16 277 4 67-82
    258 0 4 268 1 17,18 278 4 83-98
    259 0 5 269 2 19-22 279 4 99-114
    260 0 6 270 2 23-26 280 4 115-130
    261 0 7 271 2 27-30 281 5 131-162
    262 0 8 272 2 31-34 282 5 163-194
    263 0 9 273 3 35-42 283 5 195-226
    264 0 10 274 3 43-50 284 5 227-257
    265 1 11,12 275 3 51-58 285 0 258
    266 1 13,14 276 3 59-66

    Recordemos que en las lecturas de los códigos de Huffman leemos el bit más significativo en primer lugar, mientras que en los demás símbolos, incluidos bits adicionales, leemos primero el bit menos significativo.

    A continuación podemos ver el alfabeto utilizado para distancias:

    Código Bits Extra Distancia Código Bits Extra Distancia Código Bits Extra Distancia
    0 0 1 10 4 33-48 20 9 1025-1536
    1 0 2 11 4 49-64 21 9 1537-2048
    2 0 3 12 5 65-96 22 10 2049-3072
    3 0 4 13 5 97-128 23 10 3073-4096
    4 1 4,6 14 6 129-192 24 11 4097-6144
    5 1 7,8 15 6 193-256 25 11 6145-8192
    6 2 9-12 16 7 257-384 26 12 8193-12288
    7 2 13-16 17 7 385-512 27 12 12289-16384
    8 3 17-24 18 8 513-768 28 13 13685-24576
    9 3 25-32 19 8 769-1024 29 13 24577-32768

    Tenemos que tener en cuenta que una distancia puede cruzar el límite bloque para acceder a datos que se encontraban en un bloque anterior.

    El proceso de descompresión se limita a leer un código literal/distancia. Si se trata del 256, terminamos el bloque. Si es menor que este número tenemos un literal sin comprimir. Si es mayor tenemos un par <longitud, distancia>, con lo que tenemos que leer la longitud y sus códigos adicionales si fuera el caso, y la distancia con sus bits adicionales asociados. Una vez tenemos un par <longitud, distancia> debemos repetir lo que encontremos a "distancia" bytes de distancia "longitud" bytes, pudiendo ser "longitud" mayor que "distancia", caso en el que deberíamos repetir de nuevo el contenido que hay a "distancia" bytes.

    Compresión con códigos Huffman estáticos.

    Los códigos de Huffman que usa son fijos y no vienen representados en los datos. Sus longitudes de código son:

    Valor Bits Códigos
    0 --> 143 8 00110000 - 10111111
    144 --> 255 9 110010000 - 111111111
    256 --> 279 7 0000000 - 0010111
    280 --> 287 8 11000000 - 11000111

    En la tabla anterior los códigos en sí no son necesarios, puesto que pueden extraerse de las longitudes de código usando el algoritmo anteriormente expuesto.

    Las distancias se representan mediante códigos de 5 bits, por lo tanto al no ser códigos de Huffman leemos primero el bit menos significativo. Además pueden haber bits adicionales (ver tabla distancias).

    Compresión con códigos Huffman dinámicos.

    En el caso de los códigos Huffman dinámicos éstos se almacenan antes de los datos comprimidos. Tanto el código de literales/longitudes como el código de distancias se almacena por sus longitudes de código. Estas longitudes de código a su vez están comprimidas usando otro código Huffman, que también se almacena por sus longitudes de código. Veamos detalladamente cual es la organización.

    Los 5 primeros bits (HLIT-257) indican el número de códigos literales/longitud (257-286). Los 5 bits siguientes (HDIST-1) indican el número de códigos de distancia (1-32). Los 4 bits siguientes (HCLEN-4) indican el número de códigos de longitud de código (4-19).

    Ahora leeremos HCLEN números de 3 bits, que indican las longitudes de código para construir el alfabeto de longitudes de código. Una longitud de 0 indica que ese código no se da en bloque. Los elementos restantes hay que suponerlos 0. Sin embargo, estos números que hemos leído no están ordenados, sino que para conseguir una mayor compresión están almacenados en otro orden, en concreto en el orden: 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15. Por lo tanto tendremos que hacer el correspondiente intercambio. Una vez hayamos leído y colocado en su sitio las longitudes de código estaremos en disposición de generar los códigos de Huffman con el algoritmo previamente mencionado. Este código de Huffman recién generado nos servirá para leer los códigos de literal/longitud y distancia.

    Ahora debemos ir leyendo códigos de Huffman correspondientes al alfabeto recién generado y usando la siguiente correspondencia generaremos HLIT longitudes de código de códigos de literal/longitud:

    0 - 15: Representa longitudes de código de 0 - 15
    16: Copia la longitud de código anterior 3 - 6 veces.
    Los 2 bits siguientes indican las veces que se repite (0 = 3, ... , 3 = 6).
    17: Repite una longitud de código de 0 por 3 - 10 veces.
    (3 bits extra)
    18: Repite una longitud de código de 0 por 11 - 138 veces.
    (7 bits extra)

    Y después estas longitudes de código, encontraremos HDIST longitudes de código codificadas con el mismo alfabeto. Hay que tener en cuenta que las repeticiones de longitudes de código pueden cruzar el límite HLIT-HDIST.

    Una vez tenemos las longitudes de código tanto del alfabeto literal/longitud como distancia podemos generar los correspondientes códigos de Huffman, cuya traducción se hace igual que en el caso de códigos Huffman estáticos, con los mismos bits extra, etc, con lo que ya en conocimiento de los códigos de Huffman ya estamos en disposición de descomprimir el cuerpo del bloque, siguiendo el procedimiento ya señalado.

    Un ejemplo práctico.

    Para aclarar las ideas vamos a descomprimir un fichero. Observamos en primer lugar que efectivamente se trata de un fichero PNG, y como tal comienza por los bytes "89 50 4e 47 0d 0a 1a 0a".

    Empezamos con el procesamiento de los chunks.

    CHUNK: IHDR LONGITUD: 13
    CONTENIDO DEL CAMPO DE DATOS:
    00 00 00 04 00 00 00 04 08 02 00 00 00
    CRC32: 26 93 09 29

    Leemos la longitud del chunk "00 00 00 0D" en MSB que sería 13. Se trata del chunk IHDR. El campo de datos contiene los bytes "00 00 00 04 00 00 00 04 08 02 00 00 00". De aquí podemos extraer que se trata de una imagen de 4x4 pixels, 8 bits por componente, color RGB, no entrelazado, y que usa la compresión 0 y el filtro 0 (los únicos definidos en la especificación del PNG). A continuación encontramos el Crc-32 del campo de datos: "26 93 09 29".

    CHUNK: IDAT LONGITUD: 41
    CONTENIDO DEL CAMPO DE DATOS:
    78 DA 4D 8A B9 0D 00 30 0C 84 38 C9 DF FE 0B DB 57 A4 08 15 48 C0 87 32 4B 62 6D 6C CC 94 CD 25 14 DD F3 1E 74 69 6A 04 49
    CRC32: 89 CE 03 04

    Como se trata de un campo IDAT contiene los datos comprimidos que formarán la imagen. Así pues, dado que estamos bajo el método de compresión 0, el campo de datos se corresponde con uno o más bloques zlib.

    En último lugar en el fichero encontramos un chunk que indica el final del fichero:

    CHUNK: IEND LONGITUD: 0
    CONTENIDO DEL CAMPO DE DATOS:
    CRC32: AE 42 60 82

    Vamos a descomprimir el bloque zlib contenido en el campo IDAT:

    CABECERA: 78 DA
    CUERPO: 4D 8A B9 0D 00 30 0C 84 38 C9 DF FE 0B DB 57 A4 08 15
    48 C0 87 32 4B 62 6D 6C CC 94 CD 25 14 DD F3 1E 74
    ADLER-32: 69 6A 04 49

    Si nos fijamos en los campos de la cabecera que nos interesan, tenemos que observar que en los 4 bits de menor peso del primer byte se indica el uso del método de compresión 8 (inflate/deflate), que es el único válido en los PNG al igual que en los GZIP. En el bit 5 del segundo byte indica que usamos el diccionario predeterminado, lo cual también es necesario en el formato PNG. Al final encontramos un código de detección de errores ADLER-32 ("69 6A 04 49").

    A partir de ahora vamos a necesitar los datos en forma de bits:

    01001101 10001010 10111001 00001101 00000000 00110000 00001100 10000100 00111000 11001001 11011111 11111110 00001011 11011011 01010111 10100100 00001000 00010101 01001000 11000000 10000111 00110010 01001011 01100010 01101101 01101100 11001100 10010100 11001101 00100101 00010100 11011101 11110011 00011110 01110100

    Leemos el primer bit del primer byte (1). Esto indica que este bloque es el último. Leemos los dos bits siguientes (10), que indican compresión usando códigos dinámicos Huffman. Ahora procedemos a leer HLIT = (01001 = 9) + 257 = 266, HDIST = (01010 = 10) + 1 = 11 y HCEN = (1100 = 12) + 4 = 16.

    Leemos HCEN (16) números de 3 bits: 100, 011, 011, 011, 000, 000, 000, 000, 000, 011, 000, 011, 000, 100, 000, 010 y los colocamos en su sitio dado que en el fichero se encuentran desordenados:

    {4, 3, 3, 3, 0, 0, 0, 0, 0, 3, 0, 3, 0, 4, 0, 2 } à {3, 0, 2, 4, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 3, 3}

    Vamos ahora a construir el código de longitudes de código. Contamos el número de elementos que hay para cada longitud de código:

    bl_count = {0, 0, 1, 5, 2}

    A partir de aquí y con el algoritmo ya citado calculamos el menor valor para cada longitud de código:

    next_code = {0, 0, 0, 2, 14}

    Así pues el código generado sería:

    {010, -, 00, 1110, 011, 100, -, -, -, -, -, -, -, -, -, -, 1111, 101, 110}

    Una vez conocemos este alfabeto, estamos en disposición de descomprimir las longitudes de código del alfabeto de literales/longitudes y distancias:

    00 (2:literal), 1110 (3), 010 (0), 010 (0), 011 (4),
    1111 : 16 - leemos 2 bits extra: 01 = 1 + 3 = 4 (4 4"s),
    110 : 18 - leemos 7 bits extra: 1111111 = 127 + 11 = 138 (138 veces 0),
    110 : 18 - 1100001 = 97 + 11 = 108 (108 veces 0),
    011 (4), 011 (4), 1110 (3),
    101 : 17 - leemos 3 bits extra: 000 = 0 + 3 = 3 (3 veces 0),
    100 (5),
    101 : 17 - leemos 3 bits extra: 000 = 0 + 3 = 3 (3 veces 0),
    100 (5)

    Con lo que las longitudes quedarán así:

    {2, 3, 0, 0, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 3, 0, 0, 0, 5, 0, 0, 0, 5}

    Y el código de literales/longitudes sería:

    {00, 010, -, -, 1000, 1001, 1010, 1011, 1100, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, -, 1101, 1110, 011, -, -, -, 11110, -, -, -, 11111}

    De igual forma leemos las longitudes de código para generar el código de distancias:

    00 (2), 101 : 17 - leemos 3 bits extra: 010 = 2 + 3 = 5 (5 veces 0) 00 (2), 00 (2), 010 (0), 010 (0), 00 (2)

    Las longitudes serían:

    {2, 0, 0, 0, 0, 0, 2, 2, 0, 0, 2}

    Y el código generado:

    {00, -, -, -, -, -, 01, 10, -, -, 11}

    Ahora ya sabemos los dos alfabetos necesarios para descomprimir los datos de la imagen podemos realizar este paso:

    0, 0, <265 (11+0), 0 (1)>, 1, 5, 5, 6, 1, 1, 0, 255, <257 (3), 6 (9+1)>, 0, 255, 4, 8, 8, 6, <257 (3), 6 (9+1)>, <257 (3), 7 (13+1)>, 1, 0, 1, 4, 7, 7, 8, <261 (7), 10 (33+3)>, 0, 1, 256

    Al llegar al código 256 quiere decir que el bloque actual ha terminado. Como el bloque actual recordemos que era el último, ya tenemos todos los datos descomprimidos. Ahora sólo resta desentrelazar la imagen (si procede) y aplicar la inversa de los filtros. Según la solución a la que hemos llegado antes, los datos crudos serían (en hexadecimal):

          0     0  0  0    0  0  0    0  0  0    0  0  0
       1     5  5  6    1  1  0   ff  0  0    1  0 ff
       4     8  8  6   ff  0  0    0 ff  0    1  0  1
       4     7  7  8    0  0  0    0  0  0    1  0  1

    Si aplicamos la inversa de los filtros a cada pixel tenemos (en decimal), que sería la imagen original:

               0  0  0    0  0  0    0  0  0    0  0  0
            5  5  6    6  6  6    5  6  6    6  6  5
           13 13 12   12 13 12   12 12 12   13 12 13
           20 20 20   20 20 20   20 20 20   21 20 21

    miércoles, 18 de junio de 2008

    Traducción automática con indexación en buscadores

    Voy a compartir un trozo de código que he preparado, que se puede reutilizar en cualquier página web.

    Pongamos que tenemos una página web en español, y queremos que gente que no sepa español pueda leerla. Si no queremos mantener una versión de la misma página para cada idioma, lo más sencillo es recurrir a las traducciones automáticas. Cometen muchos errores, pero es lo más cómodo.

    En Internet he visto varios scripts html/javascript para llevar a cabo esta tarea, pero tenían 2 problemas:

  • El idioma origen es el inglés
  • Los enlaces utilizan javascript, con lo que los buscadores no sabrán encontrar los textos traducidos; sólo la versión original.
  • A partir de varios scripts que hay disponibles "por ahí", he creado el siguiente script, que generará las banderas que traducirán del español a 10 idiomas, de un modo que Google y los demás buscadores sabrán encontrar las páginas en todos los idiomas. Lo podéis ver en funcionamiento en esta misma página, arriba a la derecha.

    Para instalarlo, sólo hay que copiar en tu página web, widget de tu blog de Blogger o tu plantilla de cualquier otro gestor de contenidos, el código completo que hay a continuación, cambiando donde pone "www.nightearth.com" por la dirección de tu página web.

    <!--
    Script de traducción automática - http://www.dreamcoder.org
    Para configurarlo a tu página, reemplaza "www.nightearth.com" por la dirección de tu página
    -->
    
    <span id="flagsrow"></span>
    <script language="javascript">
    <!--
    function translate(langpair)
    {
    location.href = 'http://translate.google.com/translate?u='+escape(location.href)+'&hl=es&langpair='+langpair+'&tbb=1&ie='+(document.charset||document.characterSet);
    }
    
    function createFlag(langpair, langname, img)
    {
    return '<a href="javascript:translate(\''+langpair+'\');"><img alt="'+langname+'" width="23" src="'+img+'" height="20" />';
    }
    
    document.getElementById('flagsrow').innerHTML =
    createFlag('es|en',    'English',    'http://1.bp.blogspot.com/_XGtsagQTuUQ/RZk8DP2JNiI/AAAAAAAAADk/eRxHS5s1f10/s320/english.jpg') +
    createFlag('es|de',    'German',     'http://photos1.blogger.com/img/43/1633/320/13539933_041ca1eda2.jpg') +
    createFlag('es|pt',    'Portuguese', 'http://photos1.blogger.com/img/43/1633/320/13539966_0d09b410b5.jpg') +
    createFlag('es|it',    'Italian',    'http://photos1.blogger.com/img/43/1633/320/13539953_0384ccecf9.jpg') +
    createFlag('es|fr',    'French',     'http://photos1.blogger.com/img/43/1633/320/13539949_e76af75976.jpg') +
    createFlag('es|ru',    'Russian',    'http://1.bp.blogspot.com/_XGtsagQTuUQ/RZk8RP2JNpI/AAAAAAAAAEc/Tnq_uTX3WhY/s320/russia.gif') +
    createFlag('es|ja',    'Japanese',   'http://photos1.blogger.com/img/43/1633/320/13539955_925e6683c8.jpg') +
    createFlag('es|ko',    'Korean',     'http://photos1.blogger.com/img/43/1633/320/13539958_3c3b482c95.jpg') +
    createFlag('es|zh-CN', 'Chinese',    'http://photos1.blogger.com/img/43/1633/320/14324441_5ca5ce3423.jpg') +
    createFlag('es|ar',    'Arabic',     'http://2.bp.blogspot.com/_RrObyQ3XzcY/RchSvbOWb_I/AAAAAAAAAdw/uD4e5lDsh7A/s320/arabic-flag.gif');
    -->
    </script>
    
    <noscript>
    <a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|en&amp;tbb=1"><img alt="English" width="23" src="http://1.bp.blogspot.com/_XGtsagQTuUQ/RZk8DP2JNiI/AAAAAAAAADk/eRxHS5s1f10/s320/english.jpg" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|de&amp;tbb=1"><img alt="German" width="23" src="http://photos1.blogger.com/img/43/1633/320/13539933_041ca1eda2.jpg" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|pt&amp;tbb=1"><img alt="Portuguese" width="23" src="http://photos1.blogger.com/img/43/1633/320/13539966_0d09b410b5.jpg" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|it&amp;tbb=1"><img alt="Italian" width="23" src="http://photos1.blogger.com/img/43/1633/320/13539953_0384ccecf9.jpg" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|fr&amp;tbb=1"><img alt="French" width="23" src="http://photos1.blogger.com/img/43/1633/320/13539949_e76af75976.jpg" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|ru&amp;tbb=1"><img alt="Russian" width="23" src="http://1.bp.blogspot.com/_XGtsagQTuUQ/RZk8RP2JNpI/AAAAAAAAAEc/Tnq_uTX3WhY/s320/russia.gif" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|ja&amp;tbb=1"><img alt="Japanese" width="23" src="http://photos1.blogger.com/img/43/1633/320/13539955_925e6683c8.jpg" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|ko&amp;tbb=1"><img alt="Korean" width="23" src="http://photos1.blogger.com/img/43/1633/320/13539958_3c3b482c95.jpg" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|zh-CN&amp;tbb=1"><img alt="Chinese" width="23" src="http://photos1.blogger.com/img/43/1633/320/14324441_5ca5ce3423.jpg" height="20"/></a><a href="http://translate.google.com/translate?u=http%3A//www.nightearth.com&amp;hl=es&amp;langpair=es|ar&amp;tbb=1"><img alt="Arabic" width="23" src="http://2.bp.blogspot.com/_RrObyQ3XzcY/RchSvbOWb_I/AAAAAAAAAdw/uD4e5lDsh7A/s320/arabic-flag.gif" height="20"/></a><a href="http://www.dreamcoder.org">?</a>
    </noscript>
    
    <!-- Fin del Script de traducción automática -->

    Este script proporciona 2 versiones de las banderitas: una con javascript, que traduce la página actual, sea cual sea, y una en html, que traduce la página principal, desde la que se puede navegar a las otras páginas.