当前位置: 首页 > 知识库问答 >
问题:

剪贴板内容是混乱时,从Firefox复制和阅读使用Java在Ubuntu

宦飞
2023-03-14

一、背景

我正在尝试使用Java获取HTML数据风格的剪贴板数据。因此,我将它们从浏览器复制到剪贴板中。然后我使用java。awt。数据传输。剪贴板来获取它们。

这在视窗系统中正常工作。但是在Ubuntu中有一些奇怪的问题。最糟糕的是从火狐浏览器将数据复制到剪贴板中。

复制行为的例子

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);

     }
    }
   }
  }
 }

}

问题描述

我正在做的是,打开URLhttps://en.wikipedia.org/wiki/Germanic_umlaut在Firefox中。然后我选择“字母:ä”并将其复制到剪贴板。然后我运行我的Java程序。之后,生成的文件(仅其中一些作为示例)如下所示:

axel@arichter:~/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...>..

好的,开头的FEFF看起来像一个UTF-16BE字节顺序标记。但是什么是FFFD?为什么单个字母之间有0000字节UTF-16l的编码仅为006C。似乎所有字母都是用32位编码的。但是这对于UTF-16是错误的。所有非ASCII字符都用FFFD 0000编码,因此丢失。

axel@arichter:~/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.>.

这里的EFBF-BDEF-BFBD看起来不像任何已知的字节顺序标记。所有字母似乎都用16位编码,这是UTF-8中所需位的两倍。因此,所使用的位似乎总是需要的双重计数。参见上文中的UTF-16示例。所有非ASCII字母都被编码为EFBFBD,因此也会丢失。

axel@arichter:~/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...>..

与上述示例中的图片相同。所有字母都使用32位编码。在UTF-16中只能使用16位,使用替代项对的补充字符除外。所有非ASCII字母都用FFFD 0000编码,因此丢失。

axel@arichter:~/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...>...

只有这样才能完整。与上图相同。

所以结论是,在从Firefox复制了一些东西到Ubuntu剪贴板之后,它完全被弄乱了。至少对于HTML数据风格,以及使用Java读取剪贴板时。

使用的其他浏览器

当我使用Chromium browser作为数据源做同样的事情时,问题就会变得更小。

所以我在Chromium打开网址https://en.wikipedia.org/wiki/Germanic_umlaut。然后我在那里选择“字母:ä”并将其复制到剪贴板中。然后我运行我的Java程序。

结果看起来像:

axel@arichter:~/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有更多的超文本标记语言围绕在剪贴板中选择的超文本标记语言数据口味。但编码看起来正确。也为非ASCIIä=00E4。但也有一个小问题,有额外的字节0000在结束不应该有。在UTF-16有2额外的00字节在结束。

axel@arichter:~/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>.

同上。编码正确地查找UTF-8。但这里还有一个附加的00字节,不应该在那里。

环境

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)

问题

我的代码有问题吗?

有人能建议如何避免在剪贴板中出现混乱的内容吗?由于非ASCII字符丢失,至少在从Firefox复制时,我认为我们无法修复此内容。

这是一个已知的问题吗?有人能确认同样的行为吗?如果是,火狐中是否已经有关于这个问题的错误报告?

还是只有当Java代码读取剪贴板内容时才会出现这个问题?似乎是。因为如果我从Firefox复制内容并将其粘贴到Libreoffice Writer中,那么Unicode就会正确显示。如果我将内容从Writer复制到剪贴板,并使用我的Java程序读取它,那么UTF编码是正确的,除了末尾的额外00字节。所以从Writer复制的剪贴板内容的行为就像从浏览器复制Chromium内容一样。

新见解

字节0xFFFD似乎是Unicode字符“替换字符”(U FFFD)。因此,0xFDFF是这个的小端表示,0xEFBFBD是这个的UTF-8编码。所以所有的结果似乎都是错误解码和重新编码Unicode的结果。

似乎Firefox的剪贴板内容总是UTF-16LEBOM。但是Java将其作为UTF-8获取。因此,2字节的BOM变成了两个混乱的字符,它们被0xEFBFBD替换,每个额外的0x00序列变成它们自己的NUL字符,所有不正确的UTF-8字节序列变成混乱的字符,被0xEFBFBD替换。然后这个伪UTF-8将被重新编码。现在垃圾已经全部清理完毕。

例子:

在带有BOM的UTF-16LE中的序列是0xFFFE 6100 5B02 6100 FC00 6100。

这被视为UTF-8(0xEFBFBD=不是一个正确的UTF-8字节序列)=0xEFBFBD 0xEFBFBDaNULSTXaNULaNUL

将此伪ASCII重新编码为UTF-16LE将是:0xFDFF FDFF 6100 0000 5B0200 6100 0000 FDFF 0000 6100 0000

重新编码为UTF-8的伪ASCII码将是0xEFBF BDEF BFBD 6100 5B02 6100 EFBF BD00 6100

这正是发生的事情。

其他例子:

UTF-16LE中的0x00C2=C200伪UTF-8中的0xEFBFBD00

胂=0x80C2=C280伪UTF-8中的代码=0xC280

所以我认为Firefox不是这个问题的罪魁祸首,而是UbuntuJava的运行环境。因为从Firefox复制/粘贴到Writer在Ubuntu中工作,我认为Java的运行环境不能正确处理Ubuntu剪贴板中的Firefox数据味道。

新见解:

我已经比较了我的Windows 10和Ubuntu的flavormap.properties文件,这是有区别的。在Ubuntu中,text/html的本机名称是UTF8_STRING,而在Windows中,它是超文本标记语言格式。所以我想这可能是问题所在。所以我添加了一行

HTML\Format=text/HTML;字符集=utf-8;eoln=“\n”;终止符=0

到我的flavormap.properties文件在Ubuntu.

之后:

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

System.out.println(nativesForFlavors);

印刷品

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

但是当被Java读取时,Ubuntu剪贴板内容的结果没有变化。


共有2个答案

司徒杜吟
2023-03-14

由于到目前为止还没有一个有价值的答案,似乎我们需要一个丑陋的解决方法来使用Ubuntu的系统剪贴板Java。非常遗憾。哦,时间,哦,更多。我们生活在这样一个时代,Windows在使用Unicode编码方面比UbuntuLinux更好。

我们知道的已经在答案中说明了。所以我们有一个正确编码的<代码>文本/普通 结果,但是一个混乱的<代码>文本/html 结果。我们知道<代码>文本/html 结果是如何混乱的。

因此,我们可以做的是“修复”错误编码的HTML,首先用正确的替换字符替换所有混乱的字符。然后我们可以用从正确编码的纯文本中获得的正确字符替换替换字符。当然,这只适用于HTML的可见文本部分,而不在属性中。因为属性内容当然不在纯文本中。

解决方法:

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);

 }

}
康秋月
2023-03-14

在仔细研究这一点之后,这似乎是Java的一个长期缺陷(这里甚至有更老的报告)。

看起来,对于X11 Java组件,剪贴板数据总是使用UTF-8编码,Firefox使用UTF-16编码。由于这些假设,Java通过强制将UTF-16解析为UTF-8来破坏文本。我试过了,但找不到绕过这个问题的好办法。“text/html”的“text”部分似乎向Java表明,从剪贴板接收到的字节应该始终首先被解释为文本,然后以各种风格提供。我找不到任何直接的方法从X11访问预转换的字节数组。

 类似资料:
  • 问题内容: 我已经实现了回收卡视图,并想使用按钮单击方法来复制cardView的内容。cardview中有两个文本,我只想复制内容,不同的卡片不同。我怎样才能做到这一点?这是我的Cardview适配器。 v.content.getText()。toString(),无法解析内容。 问题答案: 尝试以下

  • 问题内容: 当我尝试使用粘贴到单元格时,我要粘贴的单元格保持空白,但setValueAt()似乎可以正常工作。另外,当我尝试从一个单元格剪切或复制JPopupMenu时,如果要粘贴到另一个单元格,则我的“粘贴”选项将保持禁用状态。我不知道为什么。我的代码如下。 Here’s my code for 问题答案: 根据您的示例代码,并且必须填写空白,它对 我有用… Now, maybe you’d l

  • 问题是,从vim到剪贴板的复制/粘贴停止工作。我不知道为什么。。。 在中,我也没有看到和缓冲区... 这是我的。vimrc,如果需要的话。 有什么建议吗? 谢谢

  • 问题内容: 在Internet Explorer中,我可以使用剪贴板数据对象来访问剪贴板。如何在FireFox,Safari和/或Chrome中做到这一点? 问题答案: 现在,有一种方法可以轻松地在大多数现代浏览器中使用 这将复制当前选择的文本。您可以使用选择文本区域或输入字段 要无形地复制文本,您可以快速生成一个textArea,在框中修改文本,选择它,然后复制它,然后删除textArea。在大

  • 问题内容: 我正在使用此功能将URL复制到剪贴板: 在桌面浏览器上一切正常,但在iOS设备上无法正常运行,在iOS设备上我的函数成功返回,但是数据根本没有复制到剪贴板。是什么原因造成的,我该如何解决这个问题? 问题答案: 更新!iOS> = 10 看起来像是在选择范围和一些小技巧的帮助下,可以直接复制到iOS(> = 10)Safari上的剪贴板。我在iPhone 5C iOS 10.3.3和iP

  • 我正在使用这个函数将一个URL复制到剪贴板: 在桌面浏览器上一切正常,但在iOS设备上就不行了,在iOS设备上我的函数成功返回,但数据根本没有复制到剪贴板上。是什么原因造成的?我该如何解决这个问题?