21

I cannot get word-wrap to work in a VisualForce page, which is rendered as a PDF. There is a table with a column called Comments and this contains a long string without any spaces. The long string is pushing the rest table off the PDF page.

I attempted to use the CSS attribute word-wrap: break-word; but this is not working. This is because the word-wrap attribute was introduced in CSS 3 and SalesForce uses Flying Saucer to render there PDF.

Flying Saucer only supports a handful of CSS 3 attributes and the word-wrap attribute is not one of them. Flying Saucer has strong support for CSS 2.1.

Is there a way to wrap words in CSS 2.1/Flying Saucer?

Here is my VisualForce page:

<apex:page renderAs="pdf" Controller="TaskController">
<head>
.wrapWithWordBreak {
    word-wrap: break-word;
}
</head>
    <table class="bordered">
        <thead>
            <tr>
                <th>Subject</th>
                <th>Comments</th>
                <th>Date</th>
                <th>Address</th>
            </tr>
        </thead>
        <tbody>
            <apex:repeat value="{!tasks}" var="t">
            <tr>
                <td><apex:outputText value="{!t.Subject}"/></td>
                <td class="wrapWithWordBreak"><apex:outputText value="{!t.Comments}"/></td>
                <td><apex:outputField value="{!t.Date}"/></td>
                <td><apex:outputField value="{!t.Address}"/></td>
            </tr>
            </apex:repeat>
        </tbody>
    </table>
</apex:page>

Here is my controller:

public class TaskController
{
    public list<Task> tasks{get;set;}

    public TaskController()
    {
        tasks =[SELECT Subject, Comments, Date, Address from Task];
    }
}

Table that is displayed in rendered pdf:

enter image description here

1

7 Answers 7

8
+50

In the Code Coverage podcast with Josh Birk there was mention of an improved PDF rendering engine pilot at the 19 minute 30 second mark.

I couldn't find mention of it on Google or in the IdeaExchange Pilot/Beta list.

It sounds like you need to raise a ticket or contact Josh directly to get in the pilot.

Will it solve you word-wrap issue? I don't know. But getting on the pilot would be a good way to get it supported in the future.

Update Summer '17 - Use Advanced PDF to Render Visualforce Pages as PDF Files (Pilot)

2
  • 1
    If you are a partner you can get in on the pilot from p.force.com/pilots (requires community log in). Commented Oct 21, 2014 at 2:48
  • @PeterKnolle unfortunately this one isn't listed there, even though it's been added to the summer '17 release notes Commented May 29, 2017 at 16:45
5

No, this is not possible. Even the CSS 2.1 concepts that should work, for example:

white-space: pre;           /* CSS 2.0 */
white-space: pre-wrap;      /* CSS 2.1 */

are ignored. It seems like every tool that uses flying saucer has outstanding bugs around this:

https://www.google.co.uk/search?q=flying+saucer+word+wrap

I handle this server side using a wrapper class to encapsulate the task and a processed version of a text field that has hyphens or spaces inserted to break up long words. Its still a little flaky, especially when the long words appear in the middle of the text, and it does mean that the controller knows a little too much about the view, but its that or the long words spill out over the next table cell, so I put up with it.

3
  • is flying saucer what's being used for pdf generation currently? Commented May 29, 2017 at 16:31
  • 2
    Yes. Summer 17 has a new PDF renderer in pilot so hopefully we'll get an improved experience in a release or two. Commented May 31, 2017 at 11:07
  • bollocks, got on the pilot, but happened to need it for email template attachments, which isn't supported yet, hopefully soon. i ported a word wrap algorithm to apex in the meantime (see answer below) Commented Jun 2, 2017 at 15:04
3

Unfortunately this doesn't work. Salesforce lists this as a known issue marked as no fix.

Long line of text doesn't wrap properly when PDF is generated via visualforce. It also doesn't respect CSS word-wrap and word-break properties and those do not work properly when set to break-work and break-all, respectively.

Like @bobbuzzard suggests the only option is to manually break up long strings yourself, which not surprisingly is no trivial task.

Update

Salesforce has released a pilot version of their new improved pdf rendering engine (details here). Being based on a chromimum engine the output matches what debugging results in chrome and has full support for css paged media. While it does appear to have some scalability issues associated with the current version, it's stated intention is to add

broader support for modern HTML standards, such as CSS3, JavaScript, and HTML5

One might reasonably hope issues like this are marked as non-fix as they're expected to be addressed by the new and improved rendering engine.

Update 2

At least for my use case (email attachments), the new pdf rendering isn't available yet. I wrote a word wrap helper component that may be useful to others, it's based on this answer.

LineSplitter.component

<apex:component access="global" layout="none" selfClosing="true" controller="LineSplitter">
    <apex:attribute name="input" type="String" description="Input text (required)" assignTo="{!myInput}" />
    <apex:attribute name="lineLength" type="Integer" description="Line length (defaults to 70)" assignTo="{!myLineLength}"/>
    <apex:outputText value="{!output}" escape="false"/>
</apex:component>

LineSplitter.cls

/*
Developer: Ralph Callaway <[email protected]>
Description:
    Utility for LineSplitter.component, splits input into multiple lines if needed.
    Designed specifically for pdf rendering, not tested or intended for other
    purposes (although you could probably get it there).
*/
public class LineSplitter {

    public static final Integer DEFAULT_LINE_LENGTH = 70;

    // pdf renderer doesn't convert new lines into breaks, so have
    // to manually use <br/>
    public static final String NEW_LINE = '<br/>';

    // anything in here is where we try to split words if we can
    private static String SPLIT_CHARS = ' -';

    // assignTo variable names can't match attribute names, hence the 'my' prefix
    public Integer myLineLength { get; set; }
    public String myInput { get; set; }
    public String output {
        get {
            if (output == null) {
                output = splitLines(myInput, myLineLength);
            }
            return output;
        }
        private set;
    }



    // pre-split on existing line breaks before passing to line splitter
    // since vf pdf rendering ignores line breaks
    private static String splitLines(String input, Integer lineLength) {
        if (input == null) return input;
        List<String> finalLines = new List<String>();
        for (String singleLine : input.split('\n')) {
            finalLines.add(splitLinesSingleLine(singleLine, lineLength));
        }
        return String.join(finalLines, NEW_LINE);
    }

    // splits single lines, core splitting
    // special thanks to @icr for providing something that was a huge
    // stretch to get working in Apex https://stackoverflow.com/a/17635/84130
    private static String splitLinesSingleLine(String input, Integer lineLength) {
        if (lineLength == null) { lineLength = DEFAULT_LINE_LENGTH; }
        if (input == null || input.length() <= lineLength) return input;

        String[] words = explode(input, SPLIT_CHARS);

        Integer curLineLength = 0;
        List<String> strBuilder = new List<String>();
        for(Integer i = 0; i < words.size(); i += 1)
        {
            String word = words[i];
            // If adding the new word to the current line would be too long,
            // then put it on a new line (and split it up if it's too long).
            if (curLineLength + word.length() > lineLength) {
                // Only move down to a new line if we have text on the current line.
                // Avoids situation where wrapped whitespace causes emptylines in text.
                if (curLineLength > 0) {
                    strBuilder.add(NEW_LINE);
                    curLineLength = 0;
                }

                // If the current word is too long to fit on a line even on it's own then
                // split the word up.
                while (word.length() > lineLength) {
                    strBuilder.add(word.subString(0, lineLength - 1) + '-');
                    word = word.subString(lineLength - 1);
                    strBuilder.add(NEW_LINE);
                }

                // Remove leading whitespace from the word so the new line starts flush to the left.
                // ??? no trimStart() analogue in apex, trying with trim(), revisit if there are
                // issues
                // word = word.TrimStart();
                word = word.trim();
            }
            strBuilder.add(word);
            curLineLength += word.length();
        }

        String combinedLine = String.join(strBuilder, '');

        // clear out any trailing whitespace on lines
        combinedLine = combinedLine.replaceAll('\\s*<br/>', '<br/>');
        return combinedLine;
    }

    // tokenize thing into word parts based on split characters
    private static String[] explode(String str, String splitCharacters) {
        List<String> parts = new List<String>();
        if (str != null) {
            Integer startIndex = 0;
            while (true) {
                Integer preIndex = str.subString(startIndex).indexOfAny(splitCharacters);
                Integer index = startIndex + preIndex;

                if (preIndex == -1) {
                    parts.add(str.subString(startIndex));
                    break;
                }

                String word = str.subString(startIndex, index);
                String nextChar = str.subString(index, index + 1);

                // Dashes and the likes should stick to the word occuring before it. Whitespace doesn't have to.
                if (nextChar.isWhiteSpace()) {
                    parts.Add(word);
                    parts.Add(nextChar);
                } else {
                    parts.Add(word + nextChar);
                }

                startIndex = index + 1;
            }
        }
        return parts;
    }

}

LineSplitter_Test.cls

/*
Developer: Ralph Callaway <[email protected]>
Description:
    Test methods for LineSplitter.cls
*/
@isTest
private class LineSplitter_Test {

    @isTest
    private static void nullTest() {
        runTest(null, null, null);
    }

    @isTest
    private static void noSplitTest() {
        runTest('asdf', null, 'asdf');
    }

    @isTest
    private static void simpleSplit() {
        String input = 'A sample string to split.';
        Integer lineLength = 13; // split is middle of 'string'
        String output = 'A sample' + LineSplitter.NEW_LINE +
            'string to' + LineSplitter.NEW_LINE +
            'split.';
        runTest(input, lineLength, output);
    }

    @isTest
    private static void codeSplit() {
        String input = 'BEADI-MTVAD-9SEAA';
        Integer lineLength = 7;
        String output = 'BEADI-' + LineSplitter.NEW_LINE +
            'MTVAD-' + LineSplitter.NEW_LINE +
            '9SEAA';
        runTest(input, lineLength, output);
    }

    @isTest
    private static void longWord() {
        String input = 'veryverylongword';
        Integer lineLength = 10;
        String output = 'veryveryl-' + LineSplitter.NEW_LINE +
            'ongword';
        runTest(input, lineLength, output);
    }

    private static void runTest(String input, Integer lineLength, String expected) {
        LineSplitter splitter = new LineSplitter();
        splitter.myInput = input;
        splitter.myLineLength = lineLength;
        System.assertEquals(expected, splitter.output);
    }
}
2
  • 1
    I think Advanced PDF generation which pilot in summer 17 may help.
    – goodForce
    Commented May 25, 2017 at 20:27
  • 1
    Unfortunately for me the renderAs="advanced_pdf" does not solve the issue. Need to manually split the lines like Ralph has done.
    – Doug Ayers
    Commented Jan 3, 2018 at 1:44
0

I had a same problem with URL in pdf but it was a long text field. The word wrap was not working for the URL as there are no white spaces in it. The URL was not wrapping and it was going beyond the page. i used the below code to add white spaces to over come this issue.

public static string insertBreak(String hyperLink) { // hyperLink is the URL text
        final Integer linkLength = hyperLink.length();
        final Integer maxLength = 100;
        Integer varCount = (linkLength/maxLength);

        if (math.mod(linkLength, maxLength) > 0)
            varCount++;

        String fullLink = '';
        integer startValue = 0;
        integer endValue = maxLength;
        for (Integer i = varCount; i >= 0; i--) { 
            fullLink += hyperLink.substring(startValue, endValue) + ' ';
            startValue = endValue;
            endValue = (endValue + maxLength) > linkLength ? linkLength : (endValue + maxLength);
        }
        return fullLink;
    }

While rendering the page i will display the URL text with white spaces only when the page is rendered as PDF. otherwise i will display the normal URL text without white spaces.

Hope this helps.... :)

0

I was able to handle word-wrap in a Visualforce PDF page by adding 'white-space: pre-line' to the td tag style. 'white-space: normal' also worked.

            td{
                white-space: pre-line;
                word-wrap:break-word;
                overflow-wrap: break-word; 
            }

enter image description here

0

This simple method helped in various long text wrapping problems where the wrapping delimiter can be adjusted to the output wrapping limitations:

    public  String breakLongStrings( String strInput, Integer maxLength,  String wrappedDelimiter)
    {
      String finalString ='';
      // Add chunks in the maximum allowed word length blocks
      do {
        if (strInput.left(maxLength).contains(' '))
        {
           finalString += strInput.left(maxLength);
        }
        else
        {
           finalString += strInput.left(maxLength)+wrappedDelimiter; // depends on the output wrapping rules
        }
        strInput = strInput.right(strInput.length() - maxLength);
        
    } while (strInput.length() > maxLength);
    
    return finalString+strInput; //Add the leftover
}
0

I used the approach provided by Ralph Callaway and expanded it a bit.

The basic concept is that the long words must be broken down so that they are automatically wrapped by the PDF rendering engine. The best way to split them is to wrap them with <span> tags so that a word such as SomethingVeryLong is represented as <span>Somethi</span><span>ngVeryL</span><span>ong</span>.

The Visualforce page rendering engine can then use the span elements to split the word if it needs to.

Here's the complete logic:

public class UTIL_Pdf {
    /**
    * Maximum word length
    */
    private final static Integer LENGTH = 7;

    /**
    * Reduce the long strings (i.e. words) inside the provided text into more manageable pieces.
    * NOTE: the total length of the final string might get significantly larger due to this.
    *
    * @param text  String to reduce
    *
    * @return      Same string as the one passed in, but each individual long string is
    *              split and surrounded with `<span></span>` tags.
    *              e.g.ABC-Something -> <span class="avoid-wrap"><span>ABC-</span><span>Somethi</span><span>ng</span></span>
    */
    public static String stringReducer(String text) {
        if (String.isBlank(text)) {
            return '';
        }

        String retVal = text;

        for (String str : new Set<String>(stripHtml(text).unescapeHtml4().split(' '))) {
            if (str.length() > LENGTH) {
                String newString = splitWord(str, LENGTH);

                // split into 4 replace calls becuse we had an issue that the html properites were getting replaced by accident:
                // e.g. <a href='somelongurl'>somelongurl</a> <- only "somelongurl" in content should be replaced.
                retVal = retVal.replace(' ' + str + ' ', ' <span class="avoid-wrap">' + newString + '</span> ')
                    .replace('>' + str + '<', '><span class="avoid-wrap">' + newString + '</span><')
                    .replace('>' + str + ' ', '><span class="avoid-wrap">' + newString + '</span> ')
                    .replace(' ' + str + '<', ' <span class="avoid-wrap">' + newString + '</span><');//"
            }
        }

        return retVal;
    }

    /**
    * Strip HTML tags from the provided string
    */
    private static String stripHtml(String s) {
        if (String.isBlank(s)) {
            return '';
        }

        return s.replaceAll('<[^>]+>', ' ');
    }

    /**
    * Split the provided string at special characters, 
    * then at any position if the length still exceeds `maxLength`
    *
    * @param str       String to split
    * @param maxLength Max lenght of each individual part of the string
    *
    * @return          ABC-Something -> <span>ABC-</span><span>Somethi</span><span>ng</span>
    */
    private static String splitWord(String str, final Integer maxLength) {
        String newString = '';

        for (String s2 : splitStringBySpecialCharacters(str, '-;:<>/\\\u00A0')) {
            if (s2.length() > maxLength) {
                // the string is still too long, so split it further without any character consideration
                for (String s3 : splitEqually(s2, maxLength)) {
                    newString = wrapAndAppend(newString, s3);
                }
            } else {
                // the string is of acceptable length, so just wrap it in <span></span> and append it
                newString = wrapAndAppend(newString, s2);
            }
        }

        return newString;
    }

    /**
    * Split the string by special characters.
    * If the special character is not a whitespace, it will be appended to the end of the first part of the string
    * (e.g. "ABC-Something" -> "ABC-", "Something")
    *
    * @param str               string to split
    * @param splitCharacters   single string of characters by which to split the string
    *
    * @return                  splitted string
    */
    private static List<String> splitStringBySpecialCharacters(String str, String splitCharacters) {
        List<String> parts = new List<String>();

        if (str != null) {
            Integer startIndex = 0;
            while (true) {
                Integer preIndex = str.subString(startIndex).indexOfAny(splitCharacters);
                Integer index = startIndex + preIndex;

                if (preIndex == -1) {
                    parts.add(str.subString(startIndex));
                    break;
                }

                String word = str.subString(startIndex, index);
                String nextChar = str.subString(index, index + 1);

                // Dashes and the likes should stick to the word occuring before it. Whitespace doesn't have to.
                if (nextChar.isWhiteSpace() || nextChar == '\u00A0') {
                    parts.add(word);
                    parts.add(nextChar);
                } else {
                    parts.add(word + nextChar);
                }

                startIndex = index + 1;
            }
        }

        return parts;
    }

    /**
    * Split the string equally into list of strings of provided size.
    * The trailing (last) string will usually be smaller than the rest.
    *
    * @param str   string to split
    * @param size  maximum size of each part
    *
    * @return      splitted string
    */
    private static List<String> splitEqually(String str, Integer size) {
        List<String> ret = new List<String>();

        for (Integer start = 0; start < str.length(); start += size) {
            ret.add(str.substring(start, Math.min(str.length(), start + size)));
        }
        return ret;
    }

    /**
    * Wrap `toAppend` string into `<span></span>` so that it can be wrapped automatically by PDF engine
    * and append it to the end of the provided `str`.
    *
    * @param str       String to append to and then return
    * @param toAppend  String to wrap and append
    *
    * @return          str<span>toAppend</span>
    */
    private static String wrapAndAppend(String str, String toAppend) {
        if (String.isBlank(toAppend)) {
            return str;
        }

        // don't wrap &nbsp; in spans
        // as it will just increase the string size without any benefits
        // (&nbsp; will wrap on its own)
        if (toAppend == '\u00A0') {
            str += toAppend;
        } else {
            str += '<span>' + toAppend + '</span>';
        }

        return str;
    }
}

You can then pass the HTML to UTIL_Pdf.stringReducer(..) before rendering in a Visualforce Page.

I detailed the solution a bit more in this blog post.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .