Itext - text direction (left-to-right and right-to-left)

4,155 views
Skip to first unread message

Askar Kalykov

unread,
Jun 9, 2011, 3:45:46 AM6/9/11
to Flying Saucer Users
this is kind of repost of my question on stackoverflow (http://
stackoverflow.com/questions/6288622/flyingsaucer-ltr-rtl-bidi-problem)

I know you don't support RTL by default, but I also know that IText
supports it in ColumnText and PdfPTable elements (http://
sourceforge.net/projects/itextpdf/files/Examples/examples-155/
examples-155.zip/download, arabic_hebrew.java), and that works fine
for me.
I tried to use ReplacedElementFactory but could not find any samples
on how to implement text replacements through ReplacedElement.

This is what I want to do in ReplacedElement's paint(RenderingContext
c, ITextOutputDevice outputDevice, BlockBox box) method:

PdfContentByte cb = outputDevice.getCurrentPage();
ColumnText ct = new ColumnText(cb);
int urx = box.getAbsX() + box.getWidth();
int ury = box.getAbsY() + box.getHeight();

ct.setSimpleColumn(box.getAbsX(), box.getAbsY(), urx,
ury);
ct.setSpaceCharRatio(PdfWriter.NO_SPACE_CHAR_RATIO);
ct.setRunDirection(PdfWriter.RUN_DIRECTION_LTR);
ct.addText(new Chunk(text, new com.itextpdf.text.Font(bf,
16)));
ct.go();

I can't see what it prints, I think the problem is in
ct.setSimpleColumn method call.
Could you please help what am I doing wrong?

Peter Brant

unread,
Jun 9, 2011, 3:38:08 PM6/9/11
to flying-sa...@googlegroups.com
That's a super cool idea. You're right about where the problem is.

Box coordinates and dimensions are measured in document coordinates
(which puts the origin at the upper left corner of page one and
extends over all pages). I'm not familiar with the ColumnText API,
but I assume it uses PDF coordinates which put the origin at the lower
left corner. Also, document coordinates use units of points *
dotsPerPoint (from ITextRenderer) whereas PDF coordinates are in
points (by default).

In short, you need to transform document coordinates to PDF
coordinates. This can get pretty hairy (see
ITextOutputDevice#drawImage() or ITextOutputDevice#drawSting()), but I
think in this case you should be able to just measure how far away the
bottom edge of the box is from the bottom of page and divide by
dotsPerPoint to get lly, llx is box.getAbsX() / dotsPerPoint, and urx
and ury follow from width and height divided by dotsPerPoint.

Pete

Askar Kalykov

unread,
Jun 10, 2011, 12:33:17 AM6/10/11
to Flying Saucer Users
Thanks for reply, I'm now diving in it and have another question - how
can I get current element font and color? using method above, if I
print a text in a div with border-color: red, text also will be
printed in red.

Askar Kalykov

unread,
Jun 10, 2011, 1:46:00 AM6/10/11
to Flying Saucer Users
Here is my code for transforming xhtmlrenderer into itext coordinates:
private void setupColumnCoordinates(RenderingContext c,
ITextOutputDevice outputDevice, BlockBox box){
PageBox page = c.getPage();
float dotsPerPoint = outputDevice.getDotsPerPoint();
float marginBorderPaddingLeft = page.getMarginBorderPadding(c,
CalculatedStyle.LEFT) ;
float marginBorderPaddingBottom =
page.getMarginBorderPadding(c, CalculatedStyle.BOTTOM) ;

llx = (int) ((box.getAbsX() + marginBorderPaddingLeft) /
dotsPerPoint);
urx = (int) ((box.getAbsX() + box.getWidth() +
marginBorderPaddingLeft)/dotsPerPoint);
float dist = (page.getBottom() - box.getAbsY() -
marginBorderPaddingBottom); //from box top to page bottom
ury = (int) (dist / dotsPerPoint);
lly = (int) ((dist - box.getHeight())/dotsPerPoint);
}

Looks like it works, but now I stuck in calculating widths and heights
for my replaced element.
Could you please give me a hint on how one should calculate
paragraph's bounds?


On Jun 10, 1:38 am, Peter Brant <peter.br...@gmail.com> wrote:

Askar Kalykov

unread,
Jun 14, 2011, 10:14:36 AM6/14/11
to Flying Saucer Users
finally I sort of solved the problem. First, I played with widths and
heights with coefficients. Y-coordinate is correct enough. Second, I
can ajust exact width, height, font size, font color, alignment,
direction via attributes to my div for which I'm doing replacement
(ReplacedElement).
In markup I sometimes adjust font-size/color/width, but in general
this solution is acceptable for me.

On Jun 10, 1:38 am, Peter Brant <peter.br...@gmail.com> wrote:

Askar Kalykov

unread,
Jun 16, 2011, 12:38:47 PM6/16/11
to Flying Saucer Users
Couldn't find where to post sources, so I'll just write it here.

Here is my code for ReplacedElementFactory:

public class RTLTextReplacedElementFactory implements
ReplacedElementFactory{

private String cssClassName;

private ITextReplacedElementFactory defaultFactory;

public RTLTextReplacedElementFactory(ITextOutputDevice
outputDevice, String cssClassName) {
ValidatorUtil.checkNotNull(outputDevice, "outputDevice");
ValidatorUtil.checkNotNull(cssClassName, "cssClassName");
defaultFactory = new
ITextReplacedElementFactory(outputDevice);
this.cssClassName = cssClassName;
}

@Override
public ReplacedElement createReplacedElement(LayoutContext c,
BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) {
Element element = box.getElement();
if (element == null){
return null;
}
if (element.getAttribute("class").contains(cssClassName)) {
String text = element.getTextContent().replaceAll("(?m)\\s
+", " ");
return new RTLText(c, box, uac, cssWidth, cssHeight,
text);
} else {
return defaultFactory.createReplacedElement(c, box, uac,
cssWidth, cssHeight);
}
}

@Override
public void reset() {
}

@Override
public void remove(Element e) {
}

@Override
public void setFormSubmissionListener(FormSubmissionListener
listener) {
}
}

class RTLText implements ITextReplacedElement {
private static final Logger logger =
Logger.getLogger(RTLText.class.getName());

private int width;
private int height;
private String text;
private int align;

private BaseColor color;
private float fontSize = -1;
private int direction = PdfWriter.RUN_DIRECTION_LTR;

RTLText(LayoutContext c, BlockBox box, UserAgentCallback uac, int
cssWidth, int cssHeight, String text){
this.text = text;
initDimensions(c, box, cssWidth, cssHeight);
float fontSize = box.getStyle().getFSFont(c).getSize2D();

align = com.itextpdf.text.Element.ALIGN_LEFT;
Element element = box.getElement();
String as = element.getAttribute("align");{
if (as.equalsIgnoreCase("left")) {
align = com.itextpdf.text.Element.ALIGN_LEFT;
} else if (as.equalsIgnoreCase("center")) {
align = com.itextpdf.text.Element.ALIGN_CENTER;
} else if (as.equalsIgnoreCase("right")) {
align = com.itextpdf.text.Element.ALIGN_RIGHT;
}
}
as = element.getAttribute("direction"); {
if (as.equalsIgnoreCase("ltr")){
direction = PdfWriter.RUN_DIRECTION_LTR;
} else if (as.equals("default")){
direction = PdfWriter.RUN_DIRECTION_DEFAULT;
} else if (as.equals("no-bidi")){
direction = PdfWriter.RUN_DIRECTION_NO_BIDI;
} else if (as.equals("rtl")){
direction = PdfWriter.RUN_DIRECTION_RTL;
}
}

String ecolor = element.getAttribute("color");
if (!ecolor.isEmpty()){
color = new BaseColor(Color.decode(ecolor));
}

String efontSize = element.getAttribute("font-size");
if (!efontSize.isEmpty()){
this.fontSize = Float.parseFloat(efontSize);
}
}

@Override
public int getIntrinsicWidth() {
return width;
}

@Override
public int getIntrinsicHeight() {
return height;
}

private Point location = new Point();

@Override
public Point getLocation() {
return location;
}

@Override
public void setLocation(int x, int y) {
location.x = x;
location.y = y;
}

@Override
public void detach(LayoutContext c) {
}

@Override
public boolean isRequiresInteractivePaint() {
return false;
}

@Override
public boolean hasBaseline() {
return false;
}

@Override
public int getBaseline() {
return 0;
}

@Override
public void paint(RenderingContext c, ITextOutputDevice
outputDevice, BlockBox box) {
try{
PdfWriter writer = outputDevice.getWriter();
PdfContentByte cb = writer.getDirectContent();

ITextFSFont font =
(ITextFSFont)box.getStyle().getFSFont(c);
float pdfFontSize =
outputDevice.getDeviceLength(font.getSize2D());
if (fontSize != -1){
pdfFontSize = fontSize;
}
FSColor color = box.getStyle().getColor();
BaseColor bc = null;
if (this.color != null){
bc = this.color;
} else if (color instanceof FSRGBColor) {
FSRGBColor cc = (FSRGBColor) color;
bc = new BaseColor(cc.getRed(), cc.getGreen(),
cc.getBlue());
}
ColumnText ct = new ColumnText(cb);
setupColumnCoordinates(c, outputDevice, box);
ct.setSimpleColumn(llx, lly, urx, ury);
ct.setSpaceCharRatio(PdfWriter.NO_SPACE_CHAR_RATIO);
ct.setLeading(0, 1);
ct.setRunDirection(direction);
ct.setAlignment(align);

if (bc == null){
ct.addText(new Phrase(text, new
com.itextpdf.text.Font(font.getFontDescription().getFont(),
pdfFontSize)));
} else {
ct.addText(new Phrase(text, new
com.itextpdf.text.Font(font.getFontDescription().getFont(),
pdfFontSize, 0, bc)));
}
ct.go();

} catch (DocumentException e){
logger.log(Level.WARNING, "error while processing rtl
text", e);
e.printStackTrace();
}
}

private int llx, lly, urx, ury;

private void setupColumnCoordinates(RenderingContext c,
ITextOutputDevice outputDevice, BlockBox box){
PageBox page = c.getPage();
float dotsPerPoint = outputDevice.getDotsPerPoint();
float marginBorderPaddingLeft = page.getMarginBorderPadding(c,
CalculatedStyle.LEFT) ;
float marginBorderPaddingBottom =
page.getMarginBorderPadding(c, CalculatedStyle.BOTTOM) ;

RectPropertySet margin = box.getMargin(c);
RectPropertySet padding = box.getPadding(c);

float dist = (page.getBottom() - box.getAbsY() +
marginBorderPaddingBottom); //from box top to page bottom

llx = (int) ((margin.left() + padding.left() + box.getAbsX() +
marginBorderPaddingLeft) / dotsPerPoint);
lly = (int) ((dist - box.getHeight())/dotsPerPoint);

urx = (int) ((box.getAbsX() + box.getWidth() +
marginBorderPaddingLeft)/dotsPerPoint);
ury = (int) ((dist + margin.bottom() + padding.bottom()) /
dotsPerPoint);
}

protected void initDimensions(LayoutContext c, BlockBox box, int
cssWidth, int cssHeight) {

CalculatedStyle style = box.getStyle();

Element element = box.getElement();
float scalex = 0.1f;
float scaley = 0.06f;
int lines = 1; {
String lines1 = element.getAttribute("lines");
if (!lines1.isEmpty()){
lines = Integer.parseInt(lines1);
}
}

String sx = element.getAttribute("scale-x");
if (!sx.isEmpty()){
try{
scalex = Float.parseFloat(sx);
} catch (Exception e){
System.err.println("Bad scale-x attribute value: " +
sx);
// do nothing
}
}

String sy = element.getAttribute("scale-y");
if (!sy.isEmpty()){
try{
scaley = Float.parseFloat(sy);
} catch (Exception e){
System.err.println("Bad scale-y attribute value: " +
sx);
// do nothing
}
}
String ewidth = element.getAttribute("width");
if (!ewidth.isEmpty()){
width = Integer.parseInt(ewidth) * c.getDotsPerPixel();
} else if (cssWidth != -1) {
width = cssWidth;
} else {
width = (c.getTextRenderer().getWidth(
c.getFontContext(),
style.getFSFont(c),
text)/2);
}

String eheight = element.getAttribute("height");
if (!eheight.isEmpty()){
height = Integer.parseInt(eheight) * c.getDotsPerPixel();
} else if (cssHeight != -1) {
height = cssHeight;
} else {
height = ((int) (style.getLineHeight(c) * lines));
}

width *= c.getDotsPerPixel() * scalex;
height *= c.getDotsPerPixel() * scaley;

Peter Brant

unread,
Jun 16, 2011, 12:49:09 PM6/16/11
to flying-sa...@googlegroups.com
Cool. Thanks for sharing.

Pete

AhHatem

unread,
Nov 12, 2011, 12:45:58 PM11/12/11
to flying-sa...@googlegroups.com
I see references in your code to "com.itextpdf" and I can't find some classes like "BaseColor".... Does that mean that you use a version of iText newer than 2.0.8? If yes, what about the licensing issues?

Thanks for sharing...

Peter Brant

unread,
Nov 15, 2011, 10:19:47 AM11/15/11
to flying-sa...@googlegroups.com
There is an itext5 branch on git that uses iText 5.x. master uses
iText 2.1. I don't know what the specific implications of using FS
with the AGPL'd version of iText are. I assume it would require you
to provide the source for FS too, but it's already available.

Pete

ah hatem

unread,
Nov 15, 2011, 1:02:04 PM11/15/11
to flying-sa...@googlegroups.com
As far as I understand, FS will have to be AGPL'd (or GPL v3) too and all the code that uses FS will have to be AGPL'd too ... 
I am not very good with licensing stuff though... We should probably confirm this.

Peter Brant

unread,
Nov 15, 2011, 2:35:51 PM11/15/11
to flying-sa...@googlegroups.com
Interesting. Even if an AGPL'd iText and LGPL'd FS couldn't be used
together, a commercially licensed iText and LGPL'd FS certainly could
so I think we're fine maintaining an itext5 branch regardless.

It would be interesting to know if the AGPL is really that
restrictive. If so, it would seem that would severely limit its
usefulness even in a completely open source context. For example, it
would mean that you couldn't mix AGPL'd iText with any Apache licensed
software (e.g. Spring) either.

Practically speaking, FS will remain LGPL'd for all time. It's also
worth noting that for what FS needs, iText 2.1 is perfectly
sufficient. There is no reason to upgrade unless you need iText 5.x
for something else.

Pete

ah hatem

unread,
Nov 15, 2011, 3:11:13 PM11/15/11
to flying-sa...@googlegroups.com
Actually, AFAIK, Yes, it is that restrictive...  It is designed to prevent the use of the code in any sort of non-open source code... you can't even use it in an internal system or in a web based solution like GPL... It is an amazingly restrictive licence... I am glad it is not very widely used... 

I don't need anything from itext 5 ... I only need Right To Left support... That is why I was asking about the code above if it can be used without iText 5.x ... and if yes, How?

Thanks.
Ahmed

Peter Brant

unread,
Nov 15, 2011, 4:19:06 PM11/15/11
to flying-sa...@googlegroups.com
I'm not sure where you got the code, but the master branch in git [
https://github.com/flyingsaucerproject/flyingsaucer ] uses iText
2.1.7. Any released versions will use iText 2.x as well.

Pete

Peter Brant

unread,
Nov 15, 2011, 4:21:10 PM11/15/11
to flying-sa...@googlegroups.com
Oh, as to whether Askar's code will work with iText 2.1.x, that I
don't know. iText 2.1.x did have RTL support though so I suspect it
would (or can be modified to do so).

Pete

ah hatem

unread,
Nov 15, 2011, 4:23:11 PM11/15/11
to flying-sa...@googlegroups.com
Ok, thanks.

yifei wang

unread,
Jul 24, 2012, 4:14:17 PM7/24/12
to flying-sa...@googlegroups.com
Hi Pete and Askar:

I also have to create pdfs with arabic embedded in them.

I created the RTLTextReplacedElementFactory class and imported the necessary files, but I wasn't sure how to make use of the RTLTextReplacedElementFactory class in the actual test program. Below is my attempt at this. I was sure what cssclassname is and what parameters to give the createReplacedElement method. Could you give me a little hint on that or show me a simple example on how you used it? Also, the return type of createReplacedElement and RTLText class do not seem to match. WIll this be a problem? Any help would be greatly appreciate!! Thanks a billion!!

ITextRenderer renderer = new ITextRenderer();

RTLTextReplacedElementFactory replacedElement = new RTLTextReplacedElementFactory(renderer.getOutputDevice(), cssclassName(??????));

ReplacedElement re = replacedElement.createReplacedElement(LayoutContext c(???), BlockBox box(???), UserAgentCallback uac(??), cssWidth, cssHeight); re.paint(); renderer.getSharedContext().setReplacedElementFactory(replacedElement);

Peter Brant

unread,
Jul 25, 2012, 6:09:22 PM7/25/12
to flying-sa...@googlegroups.com
renderer.getSharedContext().setReplacedElementFactory(...) is the way to install your RTLTextReplacedElementFactory instance.  Beyond that, you don't need to create ReplacedElement instances yourself.  FS will do it as required.

Pete

ahmed galal

unread,
Nov 22, 2023, 7:35:48 AM11/22/23
to Flying Saucer Users
12 years later, and this is still not solved ......
iText seems to provide RTL support with a tag price ..

anyway, if someone else got here for the same issue, There is another solution.
RTLTextReplacedElementFactory was too much for my case, and using it messed up my report. it seems it is mainly made for footers.

Instead, I extended iTextTextRenderer and used ICU4J's  BidiTransform class to reorder any string into a presentable version.


class ArabicTextRender extends ITextTextRenderer {
        @Override
        public void drawString(OutputDevice outputDevice, String string, float x, float y) {
            super.drawString(outputDevice, normalize(string), x, y);
        }

        @Override
        public void drawString(OutputDevice outputDevice, String string, float x, float y, JustificationInfo info) {
            super.drawString(outputDevice, normalize(string), x, y, info);
        }

        private String normalize(final String orig) {
            //As iText and openPdf cannot handle arabic text - and RTL languages - by default.
            //ICU4J is used to transform the text and rearrange the string arabic letters, transforming them if needed,
            //in order to show them visually inside LTR text.
            var bidiTransform = new BidiTransform();
            return bidiTransform.transform(orig,
                    (byte) Bidi.DIRECTION_LEFT_TO_RIGHT, BidiTransform.Order.LOGICAL,
                    (byte) Bidi.DIRECTION_LEFT_TO_RIGHT, BidiTransform.Order.VISUAL,
                    BidiTransform.Mirroring.ON,
                    ArabicShaping.LETTERS_SHAPE|ArabicShaping.LETTERS_MASK);
Reply all
Reply to author
Forward
0 new messages