How to use iText get pdf rendered with multiple fonts - java

I'm using iText to parse html to pdf with both english and chinese characters. I'm using
// for pdf rendering
compile group: 'com.itextpdf', name: 'itextpdf', version: '5.5.13.1'
// for pdf rendering
compile group: 'com.itextpdf.tool', name: 'xmlworker', version: '5.5.13.1'
I've already make it possible to get chinese characters parsing not a problem with dependency
// for chinese font in pdf rendering
compile group: 'com.itextpdf', name: 'itext-asian', version: '5.2.0'
and customized font provider
public class StSongProvider extends XMLWorkerFontProvider {
private static final Logger LOG = LoggerFactory.getLogger(StSongProvider.class);
public StSongProvider() {
super(null, null);
}
#Override
public Font getFont(final String fontName, String encoding, float size, final int style) {
BaseFont bfChinese = null;
try {
bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
} catch (Exception e) {
LOG.error("Not found STSong-Light,maybe com.itextpdf.itext-asian dependency problem");
}
return new Font(bfChinese, size, style);
}
}
and pdf rendring code
public static File html2Pdf(String html, String fileName) {
try {
String path = buildPath(fileName);
// step 1
Document document = new Document(PageSize.A4);
document.setMargins(20, 20, 0, 0);
// step 2
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(path));
// step 3
document.open();
// step 4
InputStream cssInput = null;
XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8)), cssInput, new StSongProvider());
// step 5
document.close();
LOG.info("PDF file: {} rendering successfully", path);
return new File(path);
} catch (IOException ex) {
// do something
} catch (DocumentException ex) {
// do something
}
}
But the english characters in the result pdf is not that beautiful without a proper font (all characters are using STSong-Light font). I want to get pdf rendered with chinese characters using STSong-Light and english characters using some fonts that iText supported originally such as Times-Roman.
I found this SO thread makes building a document possible with multiple fonts using FontSelector. But how to make it compatible with pdf creation process? The XMLWorkerHelper.getInstance().parseXHtml api only accept a FontProvider as parameter. Any ideas about this?

The solution is do something on the customized font provider, make it not return only one font, but return font depends on the html cell font-family attribute.
public class StSongProvider extends XMLWorkerFontProvider {
private static final Logger LOG = LoggerFactory.getLogger(StSongProvider.class);
public StSongProvider() {
super(null, null);
}
#Override
public Font getFont(final String fontName, String encoding, float size, final int style) {
BaseFont font = null;
try {
if (StringUtils.equals(fontName, "STSong-Light")) {
font = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
} else {
font = BaseFont.createFont(FontFactory.TIMES_ROMAN, FontFactory.defaultEncoding, true);
}
} catch (Exception e) {
// do something
}
return new Font(font, size, style);
}
}
With above font provider, and set the style="font-family:STSong-Light attribute of a html cell contains chinese characters to format them, and other english characters will be well formatted with TIMES_ROMAN;

Related

Convert HTML to PDF with Header and Footer

We have header and footer String as HTML content type but how to append both on every page.
In below java method we are passing three parameter htmlcontent, HeaderContent, FooterContent and return number of pages created in html, But where do we have to attach the header and footer content?
public static int generatePDF(String strFileName, String htmlContent,String headerHtml,String footerHtml) throws PDFNetException {
PDFDoc doc = new PDFDoc();
HTML2PDF converter = new HTML2PDF();
int nPages = 0;
try {
converter = new HTML2PDF();
doc = new PDFDoc();
converter.insertFromHtmlString(htmlContent);
try {
if (converter.convert(doc)) {
doc.save(strFileName, SDFDoc.e_linearized, null);
nPages = doc.getPageCount();
}
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (Exception e) {
ex.printStackTrace();
} finally {
converter.destroy();
doc.close();
}
return nPages;
}
One option is to post-process the PDF, by using the Stamper class, to add headers/footers.
See the following sample code on how to use Stamper call
https://www.pdftron.com/documentation/samples/#stamper
The HTML2PDF converter appends pages to the PDFDoc object passed in, so you can do the following.
call HTML2PDF.InsertFromURL(url)
call HTML2PDF.Convert(pdfdoc)
run Stamper on pages x-y stamp
and repeat to keep appending pages to pdfdoc.

How do I add an ICC to an existing PDF document

I have an existing PDF document that is using CMYK colors. It was created using a specific ICC profile, which I have obtained. The colors are obviously different if I open the document with the profile active than without. From what I can tell using a variety of tools, there is no ICC profile embedded in the document. What I would like to do is embed the ICC profile in the PDF so that it can be opened and viewed with the correct colors by third parties. My understanding is that this is possible to do with the PDF format, but nothing I have tried seems to work.
I wrote a small program using PDFBox based on looking at some examples, but it seems to have no effect. I feel like I am missing a step somewhere.
package com.mapsherpa.tools.addicc;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent;
import java.io.FileInputStream;
import java.io.IOException;
public class AddICC {
public AddICC() {
// TODO Auto-generated constructor stub
}
public static void main(String[] args) {
AddICC app = new AddICC();
try {
if( args.length != 3) {
app.usage();
} else {
app.doIt(args[0], args[1], args[2]);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
private void doIt(String input, String output, String icc) throws IOException {
// TODO Auto-generated method stub
System.out.printf("Adding %s to %s and saving as %s\n", icc, input, output);
PDDocument doc = null;
try
{
File file = new File(input);
doc = PDDocument.load(file);
PDDocumentCatalog cat = doc.getDocumentCatalog();
PDMetadata metadata = new PDMetadata(doc);
cat.setMetadata(metadata);
InputStream colorProfile = new FileInputStream(icc);
PDOutputIntent oi = new PDOutputIntent(doc, colorProfile);
oi.setInfo("SWOP (Coated), 20%, GCR, None");
oi.setOutputCondition("SWOP (Coated), 20%, GCR, None");
oi.setOutputConditionIdentifier("SWOP (Coated), 20%, GCR, None");
oi.setRegistryName("http://www.color.org");
cat.addOutputIntent(oi);
doc.save(output);
System.out.println("Finished adding color profile");
}
catch (Exception e)
{
System.out.println("Exception processing color profile");
e.printStackTrace();
}
finally
{
if (doc != null) {
doc.close();
}
}
}
private void usage() {
// TODO Auto-generated method stub
System.err.println("Usage: " + this.getClass().getName() + " <input-file> <output-file> <icc-file>");
}
}
I'm not a Java expert but I did manage to get this to run and it seems to do something but I still am not seeing the correct colors and there is no indication using imagemagick or pdfinfo that it has a color profile.
I feel like somehow I should be indicating that the document color space is ICCBased but I can't see any obvious way to do that using the PDFBox API.
Any help would be appreciated (even being told that it won't work!)
EDIT:
I believe that this is working as written in that it adds the required output intent to the document. However, I have also discovered that this is not what I need - I now believe that I need it to add an /ICCBased stream to the PDF - sigh. The updated code below is based on this stackoverflow question's updated createColorSpace function.
private static PDColorSpace createColorSpace( PDDocument doc, ColorSpace cs ) throws IOException
{
PDColorSpace retval = null;
if( cs.isCS_sRGB() )
{
retval = PDDeviceRGB.INSTANCE;
}
else if( cs instanceof ICC_ColorSpace )
{
ICC_ColorSpace ics = (ICC_ColorSpace)cs;
// CREATING MANUALLY THE COS ARR ****************************
COSArray cosArray = new COSArray();
cosArray.add(COSName.ICCBASED);
PDStream pdStream = new PDStream(doc);
cosArray.add(pdStream.getStream());
// USING DIFFERENT CONSTRUTOR *******************************
PDICCBased pdCS = new PDICCBased( cosArray );
retval = pdCS;
COSArray ranges = new COSArray();
for( int i=0; i<cs.getNumComponents(); i++ )
{
ranges.add( new COSFloat( ics.getMinValue( i ) ) );
ranges.add( new COSFloat( ics.getMaxValue( i ) ) );
}
PDStream iccData = pdCS.getPDStream();
OutputStream output = null;
try
{
output = ((COSStream)iccData.getCOSObject()).createFilteredStream();
output.write( ics.getProfile().getData() );
}
finally
{
if( output != null )
{
output.close();
}
}
pdCS.setNumberOfComponents( cs.getNumComponents() );
}
else
{
throw new IOException( "Not yet implemented:" + cs );
}
return retval;
}
private void doIt(String input, String output, String icc) throws IOException {
// TODO Auto-generated method stub
System.out.printf("Adding %s to %s and saving as %s\n", icc, input, output);
PDDocument doc = null;
try
{
File file = new File(input);
doc = PDDocument.load(file);
ICC_ColorSpace iccColorSpace = new ICC_ColorSpace(ICC_Profile.getInstance(icc));
PDColorSpace colorSpace = createColorSpace(doc, iccColorSpace);
doc.save(output);
System.out.println("Finished adding color profile");
}
catch (Exception e)
{
System.out.println("Exception processing color profile");
e.printStackTrace();
}
finally
{
if (doc != null) {
doc.close();
}
}
}
This code now has an exception:
java.io.IOException: Unknown color space number of components:-1
at org.apache.pdfbox.pdmodel.graphics.color.PDICCBased.getAlternateColorSpace(PDICCBased.java:269)
at org.apache.pdfbox.pdmodel.graphics.color.PDICCBased.loadICCProfile(PDICCBased.java:151)
at org.apache.pdfbox.pdmodel.graphics.color.PDICCBased.<init>(PDICCBased.java:89)
at com.mapsherpa.tools.addicc.AddICC.createColorSpace(AddICC.java:65)
at com.mapsherpa.tools.addicc.AddICC.doIt(AddICC.java:109)
at com.mapsherpa.tools.addicc.AddICC.main(AddICC.java:39)
at this line of code:
cosArray.add(pdStream.getStream());
The only difference I can see between this code and the other answer is that I am loading an existing PDF document rather than creating a new empty one.
For testing, I'm using the US Web (Coated) SWOP v2 icc profile from Adobe, but it is the same exception with any profile I test. From my understanding of reading the PDFBox source, it isn't a problem with the profile but with reading the stream from the document (which doesn't have an /ICCBased stream, the whole point of this question :))
EDIT 2: the code above does actually run without exceptions if used with PDFBox 1.8.10 - apparently I had linked in 2.0.0 RC2 without realizing it (total Java newbie).

Appearance issues with pdf interactive forms using iText

I have been following the 'Chapter 8: Filling out interactive forms' iText example (code and pdfs at http://itextpdf.com/examples/iia.php?id=154) and have noticed a number of appearance-related issues which also manifest themselves in the example pdfs.
The example features a list of radio buttons (classic dot within a circle) and a list of checkboxes (crossed box with a red background). So far so good. However the pdf generated after programmatically filling in these fields is visually inconsistent. The radio buttons have all reverted to squares and the selected ('Spanish') has a wingdings (?) cross within it rather than a dot. The fields are all still editable. When I select a different radio button, the previously selected radio button reverts to an empty circle. The newly selected field has changed from an empty square to a dotted circle. This can also be observed with the checkboxes.
Is there any way to fill the fields whilst preserving their original appearances?
Thanks!
EDIT - Here's a simplification of the code I use :
public void buildPdf() throws Exception {
Document document = new Document(PageSize.A4);
File file = new File("/test.pdf");
final PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(file));
document.open();
PdfPCell cell = new PdfPCell();
PdfRadioGroupTable group = new PdfRadioGroupTable("ndna", Arrays.asList("Not Available", "Not Applicable"), writer, Locale.ENGLISH, DEFAULT);
cell.addElement(group);
PdfPTable table = new PdfPTable(1);
table.addCell(cell);
cell = new PdfPCell();
cell.setCellEvent(new ActionEvent(writer, "http://localhost:8080/entrypointpdf"));
table.addCell(cell);
document.add(table);
group.addAnnotations();
document.close();
}
Here's the ActionEvent
class ActionEvent implements PdfPCellEvent {
private PdfWriter writer;
private String submissionUrl;
public ActionEvent(PdfWriter writer, String submissionUrl) {
this.writer = writer;
this.submissionUrl = submissionUrl;
}
#Override
public void cellLayout(PdfPCell cell, Rectangle position, PdfContentByte[] canvases) {
PushbuttonField submit = new PushbuttonField(writer, position, "submit") {
#Override
public PdfFormField getField() throws IOException, DocumentException {
PdfFormField field = super.getField();
field.setAction(PdfAction.createSubmitForm(submissionUrl, null, 0));
return field;
}
};
submit.setText("CLICK ME");
submit.setTextColor(BaseColor.BLUE);
submit.setFont(DEFAULT.getCalculatedBaseFont(false));
submit.setVisibility(PushbuttonField.VISIBLE);
try {
writer.addAnnotation(submit.getField());
} catch (Exception ignore) {
}
}
}
Here's the PdfRadioGroupTable
public class PdfRadioGroupTable extends PdfPTable {
private PdfWriter writer;
public PdfFormField radioGroup;
public PdfRadioGroupTable(String name, List<String> choices, PdfWriter writer, Locale locale, Font font) {
super(2);
this.writer = writer;
setWidthPercentage(100);
try {
setTotalWidth(new float[] { 14, 400 });
} catch (DocumentException ignore) {
}
radioGroup = PdfFormField.createRadioButton(writer, true);
radioGroup.setFieldName(name);
PdfPCell cell;
for (String choice : choices) {
cell = new PdfPCell();
cell.setBorder(0);
cell.setPaddingBottom(5);
cell.setUseAscender(true);
cell.setUseDescender(true);
cell.setVerticalAlignment(PdfPCell.ALIGN_CENTER);
cell.setCellEvent(new RadioCellEvent(radioGroup, choice));
addCell(cell);
cell = new PdfPCell();
cell.setBorder(0);
cell.setPaddingBottom(5);
cell.setPaddingLeft(6);
cell.setUseAscender(true);
cell.setUseDescender(true);
cell.setVerticalAlignment(PdfPCell.ALIGN_CENTER);
cell.addElement(new Phrase(choice), font));
addCell(cell);
}
}
class RadioCellEvent implements PdfPCellEvent {
protected PdfFormField radioGroup;
protected String value;
public RadioCellEvent(PdfFormField radioGroup, String value) {
this.radioGroup = radioGroup;
this.value = value;
}
#Override
public void cellLayout(PdfPCell cell, Rectangle position, PdfContentByte[] canvases) {
RadioCheckField radio = new RadioCheckField(writer, position, null, value);
radio.setChecked(false);
radio.setBorderColor(GrayColor.GRAYBLACK);
radio.setBackgroundColor(GrayColor.GRAYWHITE);
radio.setCheckType(RadioCheckField.TYPE_CIRCLE);
try {
PdfFormField field = radio.getRadioField();
writer.addAnnotation(field);
radioGroup.addKid(field);
} catch (Exception exception) {
throw new ExceptionConverter(exception);
}
}
}
}
And here's what happens on the server side when one clicks the button in the pdf (in acrobat) :
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "inline; filename=\"your.pdf\"");
try {
FdfReader fdf = new FdfReader(request.getInputStream());
File pdfFile = new File("/test.pdf");
InputStream is = new FileInputStream(pdfFile);
PdfReader reader = new PdfReader(is);
File resultFile = new File(System.currentTimeMillis() + "_" + pdfFile.getName());
FileOutputStream out = new FileOutputStream(resultFile);
PdfStamper stamper = new PdfStamper(reader, out);
AcroFields fields = stamper.getAcroFields();
fields.setFields(fdf);
// EXTRACT VALUES HERE
stamper.setFormFlattening(true);
stamper.close();
reader.close();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(FilesUtilities.read(resultFile));
OutputStream os = response.getOutputStream();
bos.writeTo(os);
os.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
So basically I generate a pdf with one radiogroup and a button. The button results in the pdf fields being imported and the idea is to send a 'receipt' back to the user in the form of the previously generated pdf containing the same fields with the user selection/values and import status fields (ok, missing, bad data, etc.) (which I have omitted in the above code as that part works fine). My problem is that the radio fields in the returned pdf don't look remotely like the editable ones in the original pdf (they don't even look like radio buttons anymore).
I tried using
fields.setFieldProperty("ndna", "setfflags", PdfFormField.FF_READ_ONLY, null);
and disabling form flattening by this results in the same alternate appearance in the returned pdf.
Your help would be most appreciated as I've been struggling with this for a while now...
Anthony
This seems to be a bug on some versions of iText. I had the same problem with iTextSharp version 5.5.5 and it was solved after I upgraded to version 5.5.9. I used "nuget" to do this. Here's a link: https://www.nuget.org/packages/iTextSharp/5.5.9

Create a non-buffered java.awt.Image from an SVG file

I have some existing code which looks a lot like the solution on Swing & Batik: Create an ImageIcon from an SVG file?
But the destination for my image is PDF, and it bugs me that when you zoom into the PDF, you see pixels. If the source and destination data are both vector graphics, it should be possible to render directly.
The library we're using (iText) takes a java.awt.Image, but I can't seem to figure out how to get a java.awt.Image which renders an SVG. Does Batik have some way to do that?
Well, here's what I ended up doing. java.awt.Image was certainly a dead-end. There was a solution in the form of wrapping up a PdfTemplate in an ImgTemplate so that it could be used as an iText Image.
(I had to have it in something which knew its size, because it's being used in a table and the layout would go completely crazy otherwise. An Image seems to know this.)
public class SvgHelper {
private final SAXSVGDocumentFactory factory;
private final GVTBuilder builder;
private final BridgeContext bridgeContext;
public SvgHelper() {
factory = new SAXSVGDocumentFactory(
XMLResourceDescriptor.getXMLParserClassName());
UserAgent userAgent = new UserAgentAdapter();
DocumentLoader loader = new DocumentLoader(userAgent);
bridgeContext = new BridgeContext(userAgent, loader);
bridgeContext.setDynamicState(BridgeContext.STATIC);
builder = new GVTBuilder();
}
public Image createSvgImage(PdfContentByte contentByte, URL resource,
float maxPointWidth, float maxPointHeight) {
Image image = drawUnscaledSvg(contentByte, resource);
image.scaleToFit(maxPointWidth, maxPointHeight);
return image;
}
public Image drawUnscaledSvg(PdfContentByte contentByte, URL resource) {
GraphicsNode imageGraphics;
try {
SVGDocument imageDocument = factory.createSVGDocument(resource.toString());
imageGraphics = builder.build(bridgeContext, imageDocument);
} catch (IOException e) {
throw new RuntimeException("Couldn't load SVG resource", e);
}
float width = (float) imageGraphics.getBounds().getWidth();
float height = (float) imageGraphics.getBounds().getHeight();
PdfTemplate template = contentByte.createTemplate(width, height);
Graphics2D graphics = template.createGraphics(width, height);
try {
// SVGs can have their corner at coordinates other than (0,0).
Rectangle2D bounds = imageGraphics.getBounds();
//TODO: Is this in the right coordinate space even?
graphics.translate(-bounds.getX(), -bounds.getY());
imageGraphics.paint(graphics);
return new ImgTemplate(template);
} catch (BadElementException e) {
throw new RuntimeException("Couldn't generate PDF from SVG", e);
} finally {
graphics.dispose();
}
}
}

Render image from servlet in flyingsaucer generated pdf

I'm using flyingsaucer to render an xhtml document to pdf through a servlet which returns the generated pdf document. The xhtml document features an image which is requested from another servlet. The image servlet checks who is logged in before returning the appropriate image. The code below shows how the image is requested:
<img height="140" width="140" src="http://localhost:8080/myapp/servlet/DisplayPic" />
My problem is that the http request for the image is from the pdf renderer and not the logged in user so the image servlet doesn't know who's logged in and therefore the desired image is not returned.
I'm currently using the code below to render the xhtml document:
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(xhtmlDocumentAsString);
renderer.layout();
os = response.getOutputStream();
renderer.createPDF(os);
I need to either maintain the user's session when the image servlet is requested or provide the renderer with the image to use for that specific xhtml element. I think the latter can be done using a ReplacedElementFactory but I haven't been able to dig out any example code that can help me.
I've got this working very nicely now. Here's the code.
In my xhtml document i have:
<div class="profile_picture" style="display:block;width:140px;height:140px;" />
(I'm using a div element instead of img as the factory is only used for block level elements)
I render my document using:
ITextRenderer renderer = new ITextRenderer();
renderer.getSharedContext().setReplacedElementFactory(new ProfileImageReplacedElementFactory(renderer.getSharedContext().getReplacedElementFactory()));
renderer.setDocumentFromString(xhtmlDocumentAsString);
renderer.layout();
os = response.getOutputStream();
renderer.createPDF(os);
And i have my own ReplacedElementFactory as below:
public class ProfileImageReplacedElementFactory implements ReplacedElementFactory {
private final ReplacedElementFactory superFactory;
public ProfileImageReplacedElementFactory(ReplacedElementFactory superFactory) {
this.superFactory = superFactory;
}
#Override
public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockBox blockBox,
UserAgentCallback userAgentCallback, int cssWidth, int cssHeight) {
Element element = blockBox.getElement();
if (element == null) {
return null;
}
String nodeName = element.getNodeName();
String className = element.getAttribute("class");
if ("div".equals(nodeName) && className.contains("profile_picture")) {
InputStream input = null;
try {
input = ...;
byte[] bytes = IOUtils.toByteArray(input);
Image image = Image.getInstance(bytes);
FSImage fsImage = new ITextFSImage(image);
if (fsImage != null) {
if ((cssWidth != -1) || (cssHeight != -1)) {
fsImage.scale(cssWidth, cssHeight);
}
return new ITextImageElement(fsImage);
}
} catch (IOException e) {
getLogger().error(ExceptionUtils.getStackTrace(e));
} catch (BadElementException e) {
getLogger().error(ExceptionUtils.getStackTrace(e));
} finally {
IOUtils.closeQuietly(input);
}
}
return superFactory.createReplacedElement(layoutContext, blockBox, userAgentCallback, cssWidth, cssHeight);
}
#Override
public void reset() {
superFactory.reset();
}
#Override
public void remove(Element e) {
superFactory.remove(e);
}
#Override
public void setFormSubmissionListener(FormSubmissionListener listener) {
superFactory.setFormSubmissionListener(listener);
}
}

Categories