PhantomJS将PDF输出到标准输出

9
我正在拼命尝试将由phantomJS生成的PDF输出到标准输出(stdout),就像这里一样。
我得到的是一个空的PDF文件,虽然它不为0大小,但显示的是一个空白页。
var page = require('webpage').create(),
system = require('system'),
address;

address = system.args[1];
page.paperSize = {format: 'A4'};

page.open(address, function (status) {
    if (status !== 'success') {
        console.log('Unable to load the address!');
        phantom.exit();
    } else {
        window.setTimeout(function () {
            page.render('/dev/stdout', { format: 'pdf' });
            phantom.exit();
        }, 1000);
    }
});

我这样调用它:phantomjs rasterize.js http://google.com>test.pdf

我尝试将/dev/stdout更改为system.stdout,但没有成功。直接将PDF写入文件可以无问题运行。

我正在寻求跨平台实现,所以我希望在非Linux系统上也能实现。


什么版本的PhantomJS?尝试升级到最新版本。 - philfreo
1
我在1.9.2 Win8x64上遇到了相同的问题。不将输出进行管道处理似乎会在控制台中有一些PDF内容,但是通过phantomjs rasterize.js > test.pdf将输出直接导出到文件中,则没有任何输出。 - Ryan Q
@philfreo 我在Win7上使用了1.9.2版本。 - michaeltintiuc
你能否将生成的pdf文件上传到某个地方,这样我们可以看看pdf生成代码是否存在某些视觉问题。 - David Mulder
3个回答

15
当在Windows上向/dev/stdout//dev/stderr/写输出时,PhantomJS会经过以下步骤(如\phantomjs\src\webpage.cpp中的render方法所示):
  1. 如果不存在/dev/stdout//dev/stderr/,则分配临时文件路径。
  2. 使用临时文件路径调用renderPdf
  3. 将网页渲染到此文件路径。
  4. 读取此文件的内容为QByteArray
  5. 对字节数组调用QString::fromAscii并写入stdoutstderr
  6. 删除临时文件。
首先,我构建了PhantomJS的源代码,但注释掉了文件删除操作。在下一次运行时,我能够检查它生成的临时文件,结果完全正常。我还尝试使用相同的结果运行phantomjs.exe rasterize.js http://google.com > test.png。这立即排除了渲染问题或与PDF有关的任何特定问题,这意味着问题必须与将数据写入stdout的方式有关。
到这个阶段,我怀疑是否存在一些文本编码的诡计。从之前的运行中,我有同一个文件(在此情况下为PNG)的有效和无效版本。
使用一些C#代码,我进行了以下实验:
//Read the contents of the known good file.
byte[] bytesFromGoodFile = File.ReadAllBytes("valid_file.png");
//Read the contents of the known bad file.
byte[] bytesFromBadFile = File.ReadAllBytes("invalid_file.png");

//Take the bytes from the valid file and convert to a string
//using the Latin-1 encoding.
string iso88591String = Encoding.GetEncoding("iso-8859-1").GetString(bytesFromGoodFile);
//Take the Latin-1 encoded string and retrieve its bytes using the UTF-8 encoding.
byte[] bytesFromIso88591String = Encoding.UTF8.GetBytes(iso88591String);

//If the bytes from the Latin-1 string are all the same as the ones from the
//known bad file, we have an encoding problem.
Debug.Assert(bytesFromBadFile
    .Select((b, i) => b == bytesFromIso88591String[i])
    .All(c => c));

请注意,我使用了ISO-8859-1编码,因为QT将其作为c-字符串的默认编码。事实证明,所有这些字节都是相同的。这个练习的目的是看看我是否能够模拟导致有效数据变为无效数据的编码步骤。

为了进一步证明,我调查了\phantomjs\src\system.cpp\phantomjs\src\filesystem.cpp

system.cpp中,System类包含对stdoutstdinstderrFile对象的引用,这些对象被设置为使用UTF-8编码。当写入stdout时,会调用File对象的write函数。该函数支持向文本文件和二进制文件写入,但由于System类初始化它们的方式,所有写入将被视为写入文本文件。因此,问题归结为:我们需要对stdout执行二进制写入,然而我们的写入最终被视为文本,并且应用了一种编码,导致生成的文件无效。
鉴于上述问题,我无法想到在Windows上实现您想要的方式而不更改PhantomJS代码的方法。因此,以下是更改的内容:

第一个更改将提供一个函数,我们可以在File对象上调用以显式执行二进制写入。

请在\phantomjs\src\filesystem.h中添加以下函数原型:

bool binaryWrite(const QString &data);

并将其定义放在\phantomjs\src\filesystem.cpp中(该方法的代码来自于此文件中的write方法):

bool File::binaryWrite(const QString &data)
{
    if ( !m_file->isWritable() ) {
        qDebug() << "File::write - " << "Couldn't write:" << m_file->fileName();
        return true;
    }

    QByteArray bytes(data.size(), Qt::Uninitialized);
    for(int i = 0; i < data.size(); ++i) {
        bytes[i] = data.at(i).toAscii();
    }
    return m_file->write(bytes);
}

\phantomjs\src\webpage.cpp的大约920行左右,您将看到一个类似于以下代码块的代码:

    if( fileName == STDOUT_FILENAME ){
#ifdef Q_OS_WIN32
        _setmode(_fileno(stdout), O_BINARY);            
#endif      

        ((File *)system->_stderr())->write(QString::fromAscii(name.constData(), name.size()));

#ifdef Q_OS_WIN32
        _setmode(_fileno(stdout), O_TEXT);
#endif          
    }

将其更改为:

   if( fileName == STDOUT_FILENAME ){
#ifdef Q_OS_WIN32
        _setmode(_fileno(stdout), O_BINARY);
        ((File *)system->_stdout())->binaryWrite(QString::fromAscii(ba.constData(), ba.size()));
#elif            
        ((File *)system->_stderr())->write(QString::fromAscii(name.constData(), name.size()));
#endif      

#ifdef Q_OS_WIN32
        _setmode(_fileno(stdout), O_TEXT);
#endif          
    }

那么代码替换所做的就是调用我们的新的binaryWrite函数,但是会通过#ifdef Q_OS_WIN32块进行保护。我这样做是为了在非Windows系统上保留旧功能,因为它们似乎没有出现这个问题(或者说确实没有?)。请注意,此修复仅适用于写入stdout - 如果您想要,您总是可以将其应用于stderr,但在那种情况下可能并不重要。
如果您只想要预构建的二进制文件(谁不想呢?),您可以在我的SkyDrive上找到带有这些修复的phantomjs.exe。我的版本大约为19MB,而我之前下载的版本只有约6MB,虽然我遵循了here中的说明,所以应该没问题。

这太棒了,非常感谢您的帮助、时间和精力投入到这个答案中! - michaeltintiuc

8

是的,ISO-8859-1是QT的默认编码,因此您需要在命令行中添加所需的参数--output-encoding=ISO-8859-1,以便PDF输出不会损坏。

例如:

phantomjs.exe rasterize.js --output-encoding=ISO-8859-1 < input.html > output.pdf

rasterize.js的代码如下(已测试,在Unix和Windows上均可运行):

var page = require('webpage').create(),
system = require('system');

page.viewportSize = {width: 600, height: 600};
page.paperSize = {format: 'A4', orientation: system.args[1], margin: '1cm'};

page.content = system.stdin.read();

window.setTimeout(function () {
    try {
        page.render('/dev/stdout', {format: 'pdf'});
    }
    catch (e) {
        console.log(e.message + ';;' + output_file);
    }
    phantom.exit();
}, 1000);

或者你可以使用stdout设置编码,如果你正在从UTF-8流中读取,则可能还需要为stdin设置编码;

system.stdout.setEncoding('ISO-8859-1');
system.stdin.setEncoding('UTF-8');
page.content = system.stdin.read();

1
谢谢你,真是太疯狂了,这么老的问题居然有新答案,感谢你的时间!我已经有一段时间没在做那个项目了,但很快会重新开始。 - michaeltintiuc
1
system.stdout.setEncoding('ISO-8859-1'); 这行代码让我省了数小时的调试时间。非常感谢您提供这个答案! - Khan
@Khan 没问题 :) - Pinchy
1
哇,这也是在使用NReco.PhantomJS包装器的C# MVC应用程序中将PDF输出呈现到/dev/stdout的关键。 - Martin_W

0

必须将PDF输出到标准输出吗?你不能改变代码为:

var page = require('webpage').create(),
system = require('system'),
address;

address = system.args[1];
output  = system.args[2];
page.paperSize = {format: 'A4'};

page.open(address, function (status) {
    if (status !== 'success') {
        console.log('Unable to load the address!');
        phantom.exit();
    } else {
        window.setTimeout(function () {
            page.render(output, { format: 'pdf' });
            phantom.exit();
        }, 1000);
    }
});

并像这样使用它:

phantomjs rasterize.js http://google.com test.pdf

这就是我之前的解决方法,但我的想法是动态生成PDF文件。在Node-webkit和PhantomJS之间来回传递数据。 - michaeltintiuc
我会仔细查看,可能有一些字符破坏了PDF结构。 - Antonio E.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接