java - example - mozilla clipboard




El contenido del portapapeles se desordena cuando se copia desde Firefox y se lee usando Java en Ubuntu (2)

Después de ver esto un poco, parece que este es un error de larga data con Java (incluso un informe más antiguo here ).

Parece que con los componentes X11 de Java se espera que los datos del portapapeles estén siempre codificados en UTF-8 y que Firefox codifique los datos con UTF-16. Debido a las suposiciones, Java hace que maneja el texto forzando el análisis de UTF-16 como UTF-8. Lo intenté pero no pude encontrar una buena manera de evitar el problema. La parte de "texto" de "text / html" parece indicar a Java que los bytes recibidos desde el portapapeles siempre deben interpretarse como texto primero y luego ofrecerse en los distintos tipos. No pude encontrar ninguna forma directa de acceder a la matriz de bytes convertidos previamente desde X11.

Fondo

Estoy tratando de obtener los datos del portapapeles en el tipo de datos HTML utilizando Java. Así los copio en el portapapeles desde los navegadores. Entonces estoy usando java.awt.datatransfer.Clipboard para obtenerlos.

Esto funciona correctamente en los sistemas Windows. Pero en Ubuntu hay algunos problemas extraños. Lo peor es cuando se copian los datos en el portapapeles desde el navegador Firefox.

Ejemplo para reproducir el comportamiento.

Código de Java:

import java.io.*;

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;

public class WorkingWithClipboadData {

 static void doSomethingWithBytesFromClipboard(byte[] dataBytes, String paramCharset, int number) throws Exception {

  String fileName = "Result " + number + " " + paramCharset + ".txt";

  OutputStream fileOut = new FileOutputStream(fileName);
  fileOut.write(dataBytes, 0, dataBytes.length);
  fileOut.close();

 }

 public static void main(String[] args) throws Exception {

  Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

  int count = 0;

  for (DataFlavor dataFlavor : clipboard.getAvailableDataFlavors()) {

System.out.println(dataFlavor);

   String mimeType = dataFlavor.getHumanPresentableName();
   if ("text/html".equalsIgnoreCase(mimeType)) {
    String paramClass = dataFlavor.getParameter("class");
    if ("java.io.InputStream".equals(paramClass)) {
     String paramCharset = dataFlavor.getParameter("charset");
     if (paramCharset != null  && paramCharset.startsWith("UTF")) {

System.out.println("============================================");
System.out.println(paramCharset);
System.out.println("============================================");

      InputStream inputStream = (InputStream)clipboard.getData(dataFlavor);

      ByteArrayOutputStream data = new ByteArrayOutputStream();

      byte[] buffer = new byte[1024];
      int length = -1;
      while ((length = inputStream.read(buffer)) != -1) {
       data.write(buffer, 0, length);
      }
      data.flush();
      inputStream.close();

      byte[] dataBytes = data.toByteArray();
      data.close();

      doSomethingWithBytesFromClipboard(dataBytes, paramCharset, ++count);

     }
    }
   }
  }
 }

}

Descripción del problema

Lo que estoy haciendo es abrir la URL https://en.wikipedia.org/wiki/Germanic_umlaut en Firefox. Luego selecciono "letras: ä" allí y copio esto en el portapapeles. Entonces ejecuto mi programa Java. Después de eso, los archivos resultantes (solo algunos de ellos como ejemplos) se ven así:

[email protected]:~/Dokumente/JAVA/poi/poi-3.17$ xxd "./Result 1 UTF-16.txt" 
00000000: feff fffd fffd 006c 0000 0065 0000 0074  .......l...e...t
00000010: 0000 0074 0000 0065 0000 0072 0000 0073  ...t...e...r...s
00000020: 0000 003a 0000 0020 0000 003c 0000 0069  ...:... ...<...i
00000030: 0000 003e 0000 fffd 0000 003c 0000 002f  ...>.......<.../
00000040: 0000 0069 0000 003e 0000                 ...i...>..

OK, el FEFF al principio parece una UTF-16BE de orden de bytes UTF-16BE . Pero, ¿qué es la FFFD ? ¿Y por qué hay esos 0000 bytes entre las letras individuales? UTF-16 de l es solo 006C . Parece como si todas las letras estuvieran codificadas en 32 bits. Pero esto es incorrecto para UTF-16 . Y todos los caracteres que no son ASCII están codificados con FFFD 0000 y, por lo tanto, se pierden.

[email protected]:~/Dokumente/JAVA/poi/poi-3.17$ xxd "./Result 4 UTF-8.txt" 
00000000: efbf bdef bfbd 6c00 6500 7400 7400 6500  ......l.e.t.t.e.
00000010: 7200 7300 3a00 2000 3c00 6900 3e00 efbf  r.s.:. .<.i.>...
00000020: bd00 3c00 2f00 6900 3e00                 ..<./.i.>.

Aquí el EFBF BDEF BFBD no se parece a ninguna marca de orden de bytes conocida. Y todas las letras parecen codificadas en 16 bits, que es el doble de los bits necesarios en UTF-8 . Así que los bits utilizados parece ser siempre el doble conteo según sea necesario. Ver en el ejemplo UTF-16 arriba. Y todas las letras que no son ASCII están codificadas como EFBFBD y también se pierden.

[email protected]:~/Dokumente/JAVA/poi/poi-3.17$ xxd "./Result 7 UTF-16BE.txt" 
00000000: fffd fffd 006c 0000 0065 0000 0074 0000  .....l...e...t..
00000010: 0074 0000 0065 0000 0072 0000 0073 0000  .t...e...r...s..
00000020: 003a 0000 0020 0000 003c 0000 0069 0000  .:... ...<...i..
00000030: 003e 0000 fffd 0000 003c 0000 002f 0000  .>.......<.../..
00000040: 0069 0000 003e 0000                      .i...>..

La misma imagen que en los ejemplos anteriores. Todas las letras están codificadas con 32 bits. Solo se utilizarán 16 bits en UTF-16 excepto los caracteres suplementarios que usan pares sustitutos. Y todas las letras que no son ASCII están codificadas con FFFD 0000 y, por lo tanto, se pierden.

[email protected]:~/Dokumente/JAVA/poi/poi-3.17$ xxd "./Result 10 UTF-16LE.txt" 
00000000: fdff fdff 6c00 0000 6500 0000 7400 0000  ....l...e...t...
00000010: 7400 0000 6500 0000 7200 0000 7300 0000  t...e...r...s...
00000020: 3a00 0000 2000 0000 3c00 0000 6900 0000  :... ...<...i...
00000030: 3e00 0000 fdff 0000 3c00 0000 2f00 0000  >.......<.../...
00000040: 6900 0000 3e00 0000                      i...>...

Sólo para estar completo. La misma imagen que arriba.

Entonces, la conclusión es que el portapapeles de Ubuntu está totalmente desordenado después de copiar algo en él desde Firefox. Al menos para los tipos de datos HTML y al leer el portapapeles utilizando Java.

Otro navegador utilizado

Cuando hago lo mismo con el navegador Chromium como fuente de los datos, los problemas se vuelven más pequeños.

Así que abro la URL https://en.wikipedia.org/wiki/Germanic_umlaut en Chromium. Luego selecciono "letras: ä" allí y copio esto en el portapapeles. Entonces ejecuto mi programa Java.

El resultado se ve como:

[email protected]:~/Dokumente/JAVA/poi/poi-3.17$ xxd "./Result 1 UTF-16.txt" 
00000000: feff 003c 006d 0065 0074 0061 0020 0068  ...<.m.e.t.a. .h
...
00000800: 0061 006c 003b 0022 003e 00e4 003c 002f  .a.l.;.".>...<./
00000810: 0069 003e 0000                           .i.>..

Chromium tiene más HTML en torno a lo seleccionado en los tipos de datos HTML en el portapapeles. Pero la codificación se ve bien. También para el no ASCII ä = 00E4 . Pero también hay un pequeño problema, hay bytes adicionales 0000 al final que no deberían estar allí. En UTF-16 hay 2 bytes adicionales al final.

[email protected]:~/Dokumente/JAVA/poi/poi-3.17$ xxd "./Result 4 UTF-8.txt" 
00000000: 3c6d 6574 6120 6874 7470 2d65 7175 6976  <meta http-equiv
...
000003f0: 696f 6e2d 636f 6c6f 723a 2069 6e69 7469  ion-color: initi
00000400: 616c 3b22 3ec3 a43c 2f69 3e00            al;">..</i>.

Lo mismo que arriba. La codificación se ve correctamente para UTF-8 . Pero aquí también hay un 00 bytes adicional al final que no debería estar allí.

Ambiente

DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.4 LTS"


Mozilla Firefox 61.0.1 (64-Bit)


java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

Preguntas

¿Estoy haciendo algo mal en mi código?

¿Alguien puede aconsejar cómo evitar que el contenido desordenado en el portapapeles? Dado que los caracteres no ASCII se pierden, al menos cuando se copian desde Firefox, no creo que podamos reparar este contenido.

¿Es este un problema conocido de alguna manera? ¿Alguien puede confirmar el mismo comportamiento? Si es así, ¿ya hay un informe de error en Firefox sobre esto?

¿O es este un problema que solo ocurre si el código Java lee el contenido del portapapeles? Parece como si. Porque si copio contenido de Firefox y lo pego en Libreoffice Writer, entonces Unicode aparece correctamente. Y si luego copio el contenido de Writer al portapapeles y lo leo con mi programa Java, entonces las codificaciones UTF son correctas, excepto los 00 bytes adicionales al final. Por lo tanto, el contenido del portapapeles copiado de Writer se comporta como el contenido copiado desde el navegador Chromium.

Nuevas perspectivas

Los bytes 0xFFFD parecen ser el carácter de Unicode 'CARACTER DE REEMPLAZO' (U + FFFD). Entonces el 0xFDFF es la pequeña representación 0xEFBFBD de esto y el 0xEFBFBD es la codificación UTF-8 de este. Entonces, todos los resultados parecen ser resultados de una decodificación incorrecta y la recodificación de Unicode.

Parece que el contenido del portapapeles que viene de Firefox es UTF-16LE con BOM siempre. Pero luego Java obtiene esto como UTF-8 . Así que la lista de materiales de 2 bytes se convierte en dos caracteres desordenados, que se reemplazan con 0xEFBFBD, cada secuencia 0x00 adicional se convierte en sus propios caracteres NUL y todas las secuencias de bytes que no son correctas. Entonces este pseudo UTF-8 será recodificado. Ahora la basura está completa.

Ejemplo:

La secuencia aɛaüa en UTF-16LE con BOM será 0xFFFE 6100 5B02 6100 FC00 6100 .

Esto se toma como UTF-8 (0xEFBFBD = no es una secuencia de bytes UTF-8 adecuada) = 0xEFBFBD 0xEFBFBD a NUL [ STX a NUL 0xEFBFBD NUL a NUL .

Este pseudo ASCII re codificado en UTF-16LE será: 0xFDFF FDFF 6100 0000 5B00 0200 6100 0000 FDFF 0000 6100 0000

Este pseudo ASCII re codificado en UTF-8 será 0xEFBF BDEF BFBD 6100 5B02 6100 EFBF BD00 6100

Y esto es exactamente lo que sucede.

Otros ejemplos:

 = 0x00C2 = C200 en UTF-16LE = 0xEFBFBD00 en pseudo UTF-8

= 0x80C2 = C280 en UTF-16LE = 0xC280 en pseudo UTF-8

Así que creo que Firefox no tiene la culpa de esto, sino de Ubuntu o el Java de ejecución de Java . Y debido a que copiar / pegar de Firefox a Writer funciona en Ubuntu, creo que el Java de ejecución de Java no maneja los tipos de datos de Firefox en el portapapeles de Ubuntu correctamente.

Nuevas perspectivas:

He comparado los archivos flavormap.properties de mi Windows 10 y mi Ubuntu y hay una diferencia. En Ubuntu el nombre nativo del text/html es UTF8_STRING mientras que en Windows es el HTML Format . Así que pensé que este podría ser el problema. Así que he añadido la línea.

HTML\ Format=text/html;charset=utf-8;eoln="\n";terminators=0

a mi archivo flavormap.properties en Ubuntu .

Después de esto:

Map<DataFlavor,String> nativesForFlavors = SystemFlavorMap.getDefaultFlavorMap().getNativesForFlavors(
   new DataFlavor[]{
   new DataFlavor("text/html;charset=UTF-16LE")
   });

System.out.println(nativesForFlavors);

huellas dactilares

{java.awt.datatransfer.DataFlavor[mimetype=text/html;representationclass=java.io.InputStream;charset=UTF-16LE]=HTML Format}

Pero no hay cambios en los resultados del contenido del portapapeles de Ubuntu cuando es leído por Java.


Ya que no hay una respuesta valiosa hasta ahora, parece que necesitamos una solución fea para trabajar con el portapapeles del sistema de Ubuntu usando Java . Muy lástima O tempora, o mores. Vivimos en tiempos donde Windows es mejor en el uso de la codificación Unicode que Ubuntu Linux .

Lo que sabemos ya está establecido en la respuesta. Por lo tanto, tenemos un resultado de text/plain codificado correctamente, pero un resultado de text/html desordenado. Y sabemos cómo se estropea el resultado de text/html .

Entonces, lo que podríamos hacer es "reparar" el código HTML codificado incorrectamente al reemplazar primero todos los caracteres desordenados por los caracteres de reemplazo correctos. Luego podemos reemplazar los caracteres de reemplazo por los caracteres correctos que se obtienen del texto plano codificado correcto. Por supuesto, esto solo se puede hacer para la parte del HTML que es texto visible y no dentro de los atributos. Porque los contenidos de los atributos, por supuesto, no están dentro del texto plano.

Solución:

import java.io.*;

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;

import java.nio.charset.Charset;

public class WorkingWithClipboadDataBytesUTF8 {

 static byte[] repairUTF8HTMLDataBytes(byte[] plainDataBytes, byte[] htmlDataBytes) throws Exception {

  //get all the not ASCII characters from plainDataBytes
  //we need them for replacement later
  String plain = new String(plainDataBytes, Charset.forName("UTF-8"));
  char[] chars = plain.toCharArray();
  StringBuffer unicodeChars = new StringBuffer();
  for (int i = 0; i < chars.length; i++) {
   if (chars[i] > 127) unicodeChars.append(chars[i]);
  }
System.out.println(unicodeChars);

  //ommit the first 6 bytes from htmlDataBytes which are the wrong BOM
  htmlDataBytes = java.util.Arrays.copyOfRange(htmlDataBytes, 6, htmlDataBytes.length);

  //The wrong UTF-8 encoded single bytes which are not replaced by `0xefbfbd` 
  //are coincidentally UTF-16LE if two bytes immediately following each other.
  //So we are "repairing" this accordingly. 
  //Goal: all garbage shall be the replacement character 0xFFFD.

  //replace parts of a surrogate pair with 0xFFFD
  //replace the wrong UFT-8 bytes 0xefbfbd for replacement character with 0xFFFD
  ByteArrayInputStream in = new ByteArrayInputStream(htmlDataBytes);
  ByteArrayOutputStream out = new ByteArrayOutputStream();
  int b = -1;
  int[] btmp = new int[6];
  while ((b = in.read()) != -1) {
   btmp[0] = b;
   btmp[1] = in.read(); //there must always be two bytes because of wron encoding 16 bit Unicode
   if (btmp[0] != 0xef && btmp[1] != 0xef) { // not a replacement character
    if (btmp[1] > 0xd7 && btmp[1] < 0xe0) { // part of a surrogate pair
     out.write(0xFD); out.write(0xFF);
    } else {
     out.write(btmp[0]); out.write(btmp[1]); //two default bytes
    }
   } else { // at least one must be the replacelement 0xefbfbd
    btmp[2] = in.read(); btmp[3] = in.read(); //there must be at least two further bytes
    if (btmp[0] != 0xef && btmp[1] == 0xef && btmp[2] == 0xbf && btmp[3] == 0xbd ||
        btmp[0] == 0xef && btmp[1] == 0xbf && btmp[2] == 0xbd && btmp[3] != 0xef) {
     out.write(0xFD); out.write(0xFF);
    } else if (btmp[0] == 0xef && btmp[1] == 0xbf && btmp[2] == 0xbd && btmp[3] == 0xef) {
     btmp[4] = in.read(); btmp[5] = in.read();
     if (btmp[4] == 0xbf &&  btmp[5] == 0xbd) {
      out.write(0xFD); out.write(0xFF);
     } else {
      throw new Exception("Wrong byte sequence: "
      + String.format("%02X%02X%02X%02X%02X%02X", btmp[0], btmp[1], btmp[2], btmp[3], btmp[4], btmp[5]), 
      new Throwable().fillInStackTrace());
     }
    } else {
     throw new Exception("Wrong byte sequence: " 
      + String.format("%02X%02X%02X%02X%02X%02X", btmp[0], btmp[1], btmp[2], btmp[3], btmp[4], btmp[5]),
      new Throwable().fillInStackTrace());
    }
   }
  }

  htmlDataBytes = out.toByteArray();

  //now get this as UTF_16LE (2 byte for each character, little endian)
  String html = new String(htmlDataBytes, Charset.forName("UTF-16LE"));
System.out.println(html);

  //replace all of the wrongUnicode with the unicodeChars selected from plainDataBytes
  boolean insideTag = false;
  int unicodeCharCount = 0;
  char[] textChars = html.toCharArray();
  StringBuffer newHTML = new StringBuffer();
  for (int i = 0; i < textChars.length; i++) {
   if (textChars[i] == '<') insideTag = true;
   if (textChars[i] == '>') insideTag = false;
   if (!insideTag && textChars[i] > 127) {
    if (unicodeCharCount >= unicodeChars.length()) 
     throw new Exception("Unicode chars count don't match. " 
      + "We got from plain text " + unicodeChars.length() + " chars. Text until now:\n" + newHTML,
      new Throwable().fillInStackTrace());

    newHTML.append(unicodeChars.charAt(unicodeCharCount++));
   } else {
    newHTML.append(textChars[i]);
   }
  }

  html = newHTML.toString();
System.out.println(html);

  return html.getBytes("UTF-8");

 }

 static void doSomethingWithUTF8BytesFromClipboard(byte[] plainDataBytes, byte[] htmlDataBytes) throws Exception {

  if (plainDataBytes != null && htmlDataBytes != null) {

   String fileName; 
   OutputStream fileOut;

   fileName = "ResultPlainText.txt";
   fileOut = new FileOutputStream(fileName);
   fileOut.write(plainDataBytes, 0, plainDataBytes.length);
   fileOut.close();

   fileName = "ResultHTMLRaw.txt";
   fileOut = new FileOutputStream(fileName);
   fileOut.write(htmlDataBytes, 0, htmlDataBytes.length);
   fileOut.close();

   //do we have wrong encoded UTF-8 in htmlDataBytes?
   if (htmlDataBytes[0] == (byte)0xef && htmlDataBytes[1] == (byte)0xbf && htmlDataBytes[2] == (byte)0xbd 
    && htmlDataBytes[3] == (byte)0xef && htmlDataBytes[4] == (byte)0xbf && htmlDataBytes[5] == (byte)0xbd) {
    //try repair the UTF-8 HTML data bytes
    htmlDataBytes = repairUTF8HTMLDataBytes(plainDataBytes, htmlDataBytes);
          //do we have additional 0x00 byte at the end?
   } else if (htmlDataBytes[htmlDataBytes.length-1] == (byte)0x00) {
    //do repair this
    htmlDataBytes = java.util.Arrays.copyOf(htmlDataBytes, htmlDataBytes.length-1);
   }

   fileName = "ResultHTML.txt";
   fileOut = new FileOutputStream(fileName);
   fileOut.write(htmlDataBytes, 0, htmlDataBytes.length);
   fileOut.close();

  }

 }

 public static void main(String[] args) throws Exception {

  Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

  byte[] htmlDataBytes = null;
  byte[] plainDataBytes = null;

  for (DataFlavor dataFlavor : clipboard.getAvailableDataFlavors()) {

   String mimeType = dataFlavor.getHumanPresentableName();

   if ("text/html".equalsIgnoreCase(mimeType)) {
    String paramClass = dataFlavor.getParameter("class");
    if ("[B".equals(paramClass)) {
     String paramCharset = dataFlavor.getParameter("charset");
     if (paramCharset != null  && "UTF-8".equalsIgnoreCase(paramCharset)) {

      htmlDataBytes = (byte[])clipboard.getData(dataFlavor);

     }
    } //else if("java.io.InputStream".equals(paramClass)) ...

   } else if ("text/plain".equalsIgnoreCase(mimeType)) {
    String paramClass = dataFlavor.getParameter("class");
    if ("[B".equals(paramClass)) {
     String paramCharset = dataFlavor.getParameter("charset");
     if (paramCharset != null  && "UTF-8".equalsIgnoreCase(paramCharset)) {

      plainDataBytes = (byte[])clipboard.getData(dataFlavor);

     }
    } //else if("java.io.InputStream".equals(paramClass)) ...
   }
  }

  doSomethingWithUTF8BytesFromClipboard(plainDataBytes, htmlDataBytes);

 }

}




clipboard