Inserindo conteúdo PDF em documento PDF gerado "on the fly"

Caros,
usando o TinyMCE, estou gravando no banco um texto formatado (HTML).
Na geração (no braço) de um relatório, usando o iText, em dado momento recupero o conteúdo do banco e transformo para PDF, com o resultado armazenado num objeto da classe ByteArrayOutputStream. Então, este resultado, preciso adicionar ao meu documento que está sendo gerado.
Já tentei basicamente dois jeitos (e algumas variantes).
No primeiro, abaixo, o conteúdo do PDF convertido não aparece no documento final:

...
   ByteArrayOutputStream baosResumo = new ByteArrayOutputStream();
   HTML2PDF.convert(resumo, baosResumo);
   PdfReader reader = new PdfReader(baosResumo.toByteArray());
   PdfCopyFields copy = new PdfCopyFields(baos);   
   copy.addDocument(reader);
   reader.close();
...

No segundo, é gerado um erro durante a execução, ao tentar recuperar uma página com o método getImportedPage

...
   ByteArrayOutputStream baosResumo = new ByteArrayOutputStream();
   HTML2PDF.convert(resumo, baosResumo);
   PdfReader reader = new PdfReader(baosResumo.toByteArray());
   PdfCopy copy = new PdfCopy(doc, baos);
   for (int i = 1; i <= reader.getNumberOfPages(); i++) {
        copy.addPage(copy.getImportedPage(reader, i));
   }
   reader.close();
...

Os objetos doc e baos acima foram criadas no início do processo de geração do relatório:

...
   protected Document doc = new Document(PageSize.A4, 22, 22, 21, 19);
   protected ByteArrayOutputStream baos  = new ByteArrayOutputStream();
...
   pdfWriter = PdfWriter.getInstance(doc,baos);
...

O conteúdo HTML está sendo gerado perfeitamente, pois nos testes, e também foi uma das tentativas, fiz a gravação do resultado da conversão num arquivo físico.

...
   File file = File.createTempFile("tmp", ".pdf");
   HTML2PDF.convert(resumo, new FileOutputStream(file));
   PdfReader reader = new PdfReader(new FileInputStream(file));
...

Assim, pergunto, onde estou errando, ou simplesmente, como resolvo este meu caso ???

Eu escrevi um artigo enorme sobre conversão para PDF. Se tiver paciência, dê uma olhada.

Agora, sobre seu problema em particular… cadê a parte que você coloca o o byteArray no ServletOutputStream e muda o Mime type?
Outra coisa, cadê a mensagem de erro?

[]´s
Rodrigo

Uma outra forma de fazer isso seria utilizando somente os recursos do iText:

final ByteArrayInputStream xhtmlInput = new ByteArrayInputStream(templateXhtml.getBytes()); final Document document = new Document(); final HtmlParser parser = new HtmlParser(); final ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream(); PdfWriter.getInstance(document, pdfOutput); parser.go(document, xhtmlInput); return pdfOutput;

Ok, vou ler o material e fazer os testes com as sugestões apresentadas.
O trace do erro vai abaixo:

java.lang.NullPointerException
at com.lowagie.text.pdf.PdfWriter.getPdfIndirectReference(Unknown Source)
at com.lowagie.text.pdf.PdfImportedPage.(Unknown Source)
at com.lowagie.text.pdf.PdfReaderInstance.getImportedPage(Unknown Source)
at com.lowagie.text.pdf.PdfCopy.getImportedPage(Unknown Source)
at tcu.fiscalizacao.apresentacao.execucao.geraRelatorioFiscalizacao.GeradorRelatorioFiscalizacao.escrever(GeradorRelatorioFiscalizacao.java:782)
at tcu.fiscalizacao.apresentacao.execucao.geraRelatorioFiscalizacao.SvlGeraRelatorioFiscalizacao.tratarVisualizacaoRelatorio(SvlGeraRelatorioFiscalizacao.java:284)
at tcu.fiscalizacao.apresentacao.execucao.geraRelatorioFiscalizacao.SvlGeraRelatorioFiscalizacao.doGetFiscalis(SvlGeraRelatorioFiscalizacao.java:31)
at tcu.fiscalizacao.geral.SvlFiscalisExecucaoAbstrata.doGetTcu(SvlFiscalisExecucaoAbstrata.java:26)
at tcu.util.seguranca.ServletTcuAbstrata.doGetAndDoPost(ServletTcuAbstrata.java:188)
at tcu.util.seguranca.ServletTcuAbstrata.doGet(ServletTcuAbstrata.java:132)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:690)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at tcu.util.contexto.ContextFilter.doFilter(ContextFilter.java:350)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:286)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
at java.lang.Thread.run(Unknown Source)

Sobre o retorno do documento (PDF) para o browser, não existe problema. Esta edição rica usando o TinyMCE é uma melhoria na interface de alimentação para o relatório que já está sendo gerado, isto é, ao final do processo, na solução que não gera a Exception acima, o documento PDF (sem o conteúdo HTML convertido para PDF, que fica em branco) é apresentado perfeitamente numa janela do browser.

Só relembrando, não estou tendo problemas na conversão do HTML para PDF.
Estou usando uma classe cujo código foi apresentado aqui em http://www.guj.com.br/posts/list/77682.java .

public class HTML2PDF {     

   ...
         
    private static void convert(InputStream input, OutputStream out) throws DocumentException {     
        Tidy tidy = new Tidy();             
        Document doc = tidy.parseDOM(input, null);     
        ITextRenderer renderer = new ITextRenderer();     
        renderer.setDocument(doc, null);     
        renderer.layout();           
        renderer.createPDF(out);                     
    }

   ...

O que está dando problemas é quando tento incluir o resultado da conversão usando o método convert da classe HTML2PDF, num ByteArrayOutputStream, dentro do documento que está sendo gerado dinamicamente. Este ‘pedaço’ de PDF, resultado da conversão, é apenas uma parte do documento maior, também um PDF. Antes do ponto onde tento incluir já tenho o meu documento com conteúdo, e após a inclusão, devo continuar com o processo de montagem do meu relatório PDF.
As soluções que consegui encontrar para concatenação de PDF’s, usam um arquivo físico (PDF) ao qual será concatenado um outro arquivo físico, também PDF.
As duas soluções que montei, baseando-me em artigos e alguns exemplos, não funcionaram como esperado. Uma lança Exception, após chamada ao método PdfImportedPage da classe PdfCopy, e a outra, chamada ao método addDocument da classe PdfCopyFields, não lança nada, mas também não faz nada, ou pelo menos não aparece no documento PDF gerado ao final do processo.

Bom, prosseguirei com as tentativas sugeridas, trabalhando na prancheta …
Se tiver novidades, postarei.
No mais, grato pela atenção e boa vontade.

André,

tentei usar o próximo iText para fazer a conversão (que não é o meu problema, pois a conversão HTML->PDF está bem solucionada com as classes Tidy, ITextRenderer), mas vislumbrei no seu exemplo a possibilidade de que o método parse (ou go) da classe HtmlParser viesse a incluir o resultado da conversão diretamente no documento que estou gerando.

O meu código ficou

   ByteArrayInputStream bais = new ByteArrayInputStream(resumo.getBytes());
   HtmlParser.parse(doc, bais);

mas a classe HtmlParser não conseguiu fazer a conversão. O teste foi feito com o mesmo HTML usado na conversão com o Tidy e ITextRenderer, que funcionaram satisfatoriamente.
Na documentação da classe (http://www.1t3xt.info/api/index.html?com/lowagie/text/pdf/PdfReader.html) é dito que ela pode fazer o parser de alguns arquivos HTML (This class can be used to parse some HTML files), dando a entender que ela não é poderosa, e confiável, o suficiente, para uma conversão mais rica de um HTML para PDF, por exemplo, usando tabelas, listas, recuos, etc …
No caso, ela reclamou de nbsp, e assim, gostaria de saber como faço para declarar estes elementos do HTML para o HtmlParser, para continuar com os testes da mesma, isto é, saber até onde ela pode ir.

Grato e inté.

ExceptionConverter: org.xml.sax.SAXParseException: The entity “nbsp” was referenced, but not declared.
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(Unknown Source)
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.fatalError(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLScanner.reportFatalError(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanEntityReference(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(Unknown Source)
at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(Unknown Source)
at javax.xml.parsers.SAXParser.parse(Unknown Source)
at com.lowagie.text.html.HtmlParser.go(Unknown Source)
at com.lowagie.text.html.HtmlParser.parse(Unknown Source)
at tcu.fiscalizacao.apresentacao.execucao.geraRelatorioFiscalizacao.GeradorRelatorioFiscalizacao.escrever(GeradorRelatorioFiscalizacao.java:790)
at tcu.fiscalizacao.apresentacao.execucao.geraRelatorioFiscalizacao.SvlGeraRelatorioFiscalizacao.tratarVisualizacaoRelatorio(SvlGeraRelatorioFiscalizacao.java:284)
at tcu.fiscalizacao.apresentacao.execucao.geraRelatorioFiscalizacao.SvlGeraRelatorioFiscalizacao.doGetFiscalis(SvlGeraRelatorioFiscalizacao.java:31)
at tcu.fiscalizacao.geral.SvlFiscalisExecucaoAbstrata.doGetTcu(SvlFiscalisExecucaoAbstrata.java:26)
at tcu.util.seguranca.ServletTcuAbstrata.doGetAndDoPost(ServletTcuAbstrata.java:188)
at tcu.util.seguranca.ServletTcuAbstrata.doGet(ServletTcuAbstrata.java:132)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:690)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at tcu.util.contexto.ContextFilter.doFilter(ContextFilter.java:350)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:286)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
at java.lang.Thread.run(Unknown Source)

Eu tive que fazer umas adaptações aqui pra funcionar como eu queria…

[code]
// Converte de HTML para XHTML
final InputStream htmlOriginalInput = new ByteArrayInputStream(html.getBytes());
final ByteArrayOutputStream xhtmlOutput = new ByteArrayOutputStream();

	final Tidy tidy = new Tidy();
	tidy.setXHTML(true);
	tidy.setCharEncoding(Configuration.UTF8);
	tidy.parseDOM(htmlOriginalInput, xhtmlOutput);

	// Acrescenta align center para alinhar texto ao centro,
	// porque conversor de HTML para PDF não aceita text-align dentro da tag style
	String templateXhtml = new String(xhtmlOutput.toByteArray()).replace("\r\n", " ");
	Pattern p = Pattern.compile("(&lt;span|&gt;&lt;div|&gt;&lt;p|&gt;&lt;h1|&gt;&lt;h2|&gt;&lt;h3|&gt;&lt;h4|&gt;&lt;h5|&gt;&lt;h6)(.*)(text-align)");
	Matcher m = p.matcher(templateXhtml);
	StringBuffer xhtml = new StringBuffer();
	while (m.find()) {
		m.appendReplacement(xhtml, m.group(1) + " align=\"center\"" + m.group(2) + m.group(3));
	}
	m.appendTail(xhtml);
	templateXhtml = xhtml.toString();

	// Sublinhado
	p = Pattern.compile("(&lt;span)(.*)(underline)(.*)(&lt;/span&gt;)");
	m = p.matcher(templateXhtml);
	xhtml = new StringBuffer();
	while (m.find()) {
		m.appendReplacement(xhtml, "<u>" + m.group(1) + m.group(2) + m.group(3) + "</u>");
	}
	m.appendTail(xhtml);
	templateXhtml = xhtml.toString();

	// Adiciona um espaço antes de cada linha para evitar problemas com concatenação de palavaras.
	p = Pattern.compile("(\n)(.*)");
	m = p.matcher(templateXhtml);
	xhtml = new StringBuffer();
	while (m.find()) {
		m.appendReplacement(xhtml, " " + m.group(1) + m.group(2) + " ");
	}
	m.appendTail(xhtml);
	templateXhtml = xhtml.toString();

	// Converte de XHTML para PDF
	final ByteArrayInputStream xhtmlInput = new ByteArrayInputStream(templateXhtml.getBytes());
	final Document document = new Document();
	final ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream();
	final PdfWriter writer = PdfWriter.getInstance(document, pdfOutput);
	final HtmlParser parser = new HtmlParser();
	parser.go(documentListener, xhtmlInput);
	return pdfOutput;[/code]

André,
após algumas mexidas no seu código para que funcionasse aqui, inclusive um famigerado problema de índice no Matcher.group, resolvido com

e também não conseguindo definir o “char encoding”, que ficou comentado

acabei chegando a uma das limitações ao converter HTML para XHTML antes de chegar no PDF:

java.lang.ClassCastException: com.lowagie.text.Table cannot be cast to com.lowagie.text.TextElementArray
at com.lowagie.text.xml.SAXiTextHandler.handleStartingTags(Unknown Source)
at com.lowagie.text.html.SAXmyHtmlHandler.startElement(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.startElement(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanStartElement(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(Unknown Source)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(Unknown Source)
at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(Unknown Source)
at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(Unknown Source)
at javax.xml.parsers.SAXParser.parse(Unknown Source)
at com.lowagie.text.html.HtmlParser.go(Unknown Source)
at com.lowagie.text.html.HtmlParser.parse(Unknown Source)
at tcu.fiscalizacao.geral.util.HTML2PDF.convert(HTML2PDF.java:118)

Nesta conversão, aparentemente, não é possível com alguns elementos de interface, no caso, tabelas que foram criadas no TinyMCE e deverão fazer parte do texto PDF.

Assim, vou dar um tempo nesta linha de solução (HTML -> XHTML -> PDF) e retomar o caminho de adicionar páginas de um PDF em memória (ByteArrayOutputStream) a um PDF já em construção (Document).

Grato e inté

Conclusões:

  1. não se pode inserir pedaços de um conteúdo PDF em outro PDF. O iText dá suporte apenas à concatenação de páginas, não de conteúdo.
  2. a opção é adicionar os elementos do HTML diretamente no documento PDF, usando a classe HTMLWorker.
  3. para a adequada inserção e visualização dos elementos HTML, é necessário um pouco de estilos (CSS).

Assim, a partir de um HTML, a sua inclusão no documento que está sendo gerado (PDF), é feita com o seguinte código

StyleSheet ss = HTML2PDF.getEstilosRelatorioFiscalizacao("fiscalis");
					
// converte o conteúdo HTML para PDF
StringReader reader = new StringReader(html);
ArrayList<Element> elementList = HTMLWorker.parseToList(reader, ss);
for (Element element : elementList) {
	doc.add(element);
}

Nem todos os elementos HTML serão adequadamente adicionados ao documento PDF. O suporte é limitado, mas para as tags básicas, funciona perfeitamente (negrito, itálico, listas, tabelas, …).

Exemplo de estilos que deverão ser usados:

StyleSheet styleSheet = new StyleSheet();

styleSheet.loadTagStyle("body","leading","14f");
styleSheet.loadTagStyle("p","leading","14f");
styleSheet.loadTagStyle("table","leading","14f");
styleSheet.loadTagStyle("ul","leading","14f");
styleSheet.loadTagStyle("ol","leading","14f");
styleSheet.loadTagStyle("body","face","Times");
styleSheet.loadStyle("subsup","size","6px");

return styleSheet;

No meu caso, adicionalmente algumas substituições tiveram que ser feitas no código HTML gerado, pois algumas tags incluídas pelo TinyMCE não estavam sendo adequadamente ‘entendidas’ pelo PDF. Para melhor manutenção, as substituições foram colocadas num arquivo de propriedades, que são lidas durante a rotina de substituição.

Exemplo:

default.encontrar.1=(.?)<span style=“text-decoration: underline;”>(.?)(.?)
default.substituir.1=$1$2$3
default.encontrar.2=(.
?)<su([bp])>(.?)</su([bp])>(.?)
default.substituir.2=$1<su$2>$3</su$4>$5
default.encontrar.3=(.?)<p style="padding-left:(.?)">(.?)

(.?)
default.substituir.3=$1<div style=“padding-left:$2”>$3$4
default.encontrar.4=(.?)<p style="(.?);\p{Blank}padding-left:(.?)">(.?)(.?)
default.substituir.4=$1<div style="$2; padding-left:$3">$4$5

Estas substituições são feitas com replaceAll:

Bom, espero que estas orientações possam ajudar alguém e economicar algumas horas de exploração por alternativas que não vão dar em nada.