Apex

Visualforce PDFで長い文字列を行に収める

VisualforceをPDFで出力すると、改行ポイントのない長い文字列は折り返されません。取引先名をバイト数で数えて行に収める実装と、その理由を確認します。

2024.01.11

なぜCSSの折り返しだけでは足りないのか

VisualforceをrenderAs="pdf"で出力するとき、長い文字列は思った位置で折り返されません。公式ガイドも「スペースやダッシュのような改行ポイントがない長い文字列は、PDFレンダリングサービスでは折り返せない」と明記しています。取引先名のように区切りのない長い日本語の文字列を表に収めたいときは、CSSの折り返しに頼らず、あらかじめアプリ側で行を区切っておく必要があります。

実装の考え方

この実装では、取引先名をバイト数で数えて行を区切ります。全角文字はUTF-8で1文字あたり3バイト、半角文字は1バイトになるため、文字数ではなくバイト数で区切ることで、全角と半角が混ざった文字列でも、見た目の幅のおおよその目安にできます。

項目値
1行あたりの上限50バイト(CHARACTERLIMIT)
収まる行数最大3行
3行を超えるとき3行目の末尾を切り詰め...を付ける(CHARACTEROTHERS)
句点での分割「。」があれば、そこで文を区切ってから行に収める

Visualforceページ

renderAs="pdf"でPDF出力し、取引先名を3行までのapex:outputTextに分けて表示します。

<apex:page renderAs="pdf" applyHtmlTag="false" showHeader="false" standardController="Account" extensions="CharacterLimitSampleController">
    <head>
        <style type="text/css">
            @page { size: landscape; margin: 15mm; }
            body { font-family: Arial Unicode MS; font-size: 11px; }
            table.tableHeader { width: 100%; border-collapse: collapse; }
            td.labelName { width: 15%; text-align: center; border: 1px solid black; }
            td.valueName { width: 85%; padding-left: 10px; text-align: left; line-height: 16px; border: 1px solid black; }
        </style>
    </head>
    <body>
        <table class="tableHeader">
            <tr>
                <td class="labelName">取引先名</td>
                <td colspan="4" class="valueName">
                    <apex:outputPanel rendered="{!nameLine1 != ''}"><apex:outputText value="{!nameLine1}" /></apex:outputPanel>
                    <apex:outputPanel rendered="{!nameLine2 != ''}"><br/><apex:outputText value="{!nameLine2}" /></apex:outputPanel>
                    <apex:outputPanel rendered="{!nameLine3 != ''}"><br/><apex:outputText value="{!nameLine3}" /></apex:outputPanel>
                </td>
            </tr>
        </table>
    </body>
</apex:page>

フォントをArial Unicode MSにして全角文字がつぶれないようにしています。

Apexクラス

取引先名を50バイトごとに切り、最大3行ぶんの文字列を作ります。

public with sharing class CharacterLimitSampleController {
    private final Integer CHARACTERLIMIT = 50;
    private final String CHARACTEROTHERS = '...';
    private final String PERIOD = '。';
    private final Account account;
    public String nameLine1 { get; set; }
    public String nameLine2 { get; set; }
    public String nameLine3 { get; set; }
    private String inputEnd = '';

    public CharacterLimitSampleController(ApexPages.StandardController controller) {
        if (Schema.sObjectType.Account.isAccessible()) {
            account = [SELECT Name FROM Account WHERE Id = :controller.getRecord().Id];
            setLines();
        }
    }

    private void setLines() {
        List<String> lineList = getNameLines(account.Name, CHARACTERLIMIT);
        nameLine1 = lineList.get(0);
        if (lineList.size() >= 2) nameLine2 = lineList.get(1);
        if (lineList.size() == 3) {
            if (inputEnd == PERIOD) {
                nameLine3 = lineList.get(2);
            } else {
                String str = lineList.get(2);
                nameLine3 = str.left(str.length() - 1);
            }
        }
        if (lineList.size() > 3) nameLine3 = lineList.get(2);
        if (lineList.size() >= 4) nameLine3 = addEt(lineList.get(2), CHARACTERLIMIT);
    }

    private List<String> getNameLines(String input, Integer maxBytes) {
        List<String> outputList = new List<String>();
        Integer cnt = 1;
        if (String.isBlank(input)) {
            outputList.add(input);
            return outputList;
        }
        inputEnd = input.right(1);
        List<String> inputStrByPeriod = getSplit(input, PERIOD);
        if (inputStrByPeriod.isEmpty()) {
            outputList.add(input);
            return outputList;
        }
        for (String inputStr : inputStrByPeriod) {
            Integer sumBytes = 0;
            Integer bigSumBytes = 0;
            Integer index = 0;
            Integer inputStrSumBytes = getBytes(inputStr);
            for (Integer i = 0; i < inputStr.length(); i++) {
                Integer oneCharBytes = getBytes(inputStr.mid(i, 1));
                sumBytes += oneCharBytes;
                bigSumBytes += oneCharBytes;
                if (bigSumBytes >= inputStrSumBytes) {
                    outputList.add(inputStr.mid(index, i + 2 - index));
                    cnt++;
                } else if (sumBytes >= maxBytes && cnt <= 4) {
                    sumBytes = 0;
                    outputList.add(inputStr.mid(index, i + 1 - index));
                    if (cnt == 4) break;
                    index = i + 1;
                    cnt++;
                }
            }
        }
        return outputList;
    }

    private List<String> getSplit(String input, String splitter) {
        List<String> ret = new List<String>();
        for (String str : input.split(splitter)) {
            if (str.length() > 0) {
                ret.add(str + splitter);
            }
        }
        return ret;
    }

    private String addEt(String input, Integer maxBytes) {
        if (getBytes(input + '..') <= maxBytes) return input.left(input.length() - 1) + CHARACTEROTHERS;
        if (getBytes(input + '.') <= maxBytes) return input.left(input.length() - 2) + CHARACTEROTHERS;
        if (getBytes(input) <= maxBytes) return input.left(input.length() - 3) + CHARACTEROTHERS;
        return input.left(input.length() - 4) + CHARACTEROTHERS;
    }

    private Integer getBytes(String value) {
        return Blob.valueOf(value).size();
    }
}

取引先名をバイト数で区切り、3行までの文字列に分けます。読み取りのみでDMLは行いません。

Blob.valueOf().size()は、文字列をUTF-8に変換したバイト数を返します。全角の日本語はおおむね1文字3バイト、半角の英数字は1バイトになるため、「50文字」ではなく「50バイト」で区切っている点に注意してください。同じ50という上限でも、全角だけの文字列と半角だけの文字列では収まる文字数が大きく変わります。

表示のされ方

取引先名の長さ表示
50バイト以内1行目だけに表示されます
50〜150バイト程度「。」があればそこで、なければバイト数で2〜3行に分けて表示されます
3行に収まらない3行目の末尾が切り詰められ、末尾に...が付きます

ここで間違えやすい

間違い何が起きるか
文字数で区切ったつもりでバイト数の処理と混同する全角文字混じりの文字列で、想定より早く行が変わったり、逆に長く収まったりします
CSSの折り返しだけで対応しようとする改行ポイントのない長い文字列は、PDFレンダリングサービス側で折り返されません
apex:outputTextを3行より多く並べるこのコントローラは3行までしか値を詰めていないため、4行目以降は表示されません

確認した環境

  • 2026年9月 / Salesforce Summer '26(APIバージョン67.0)時点の公式ガイドで、PDFレンダリングの折り返しに関する制約を確認しています
  • バイト数の区切り位置は、ご自身の組織のデータで確認してください

まとめ

  • VisualforceをrenderAs="pdf"で出力すると、改行ポイントのない長い文字列は折り返されません
  • この実装は、取引先名をUTF-8のバイト数で数えて、あらかじめ3行までの文字列に区切ります
  • 全角はおおむね3バイト、半角は1バイトなので、「50文字」ではなく「50バイト」で区切っています
  • 「。」があれば、そこで文を区切ってから行に収めます
  • 3行に収まらない場合は、3行目の末尾を切り詰めて...を付けます
  • CSSの折り返しに頼らず行を区切っておくのは、PDFレンダリングサービスが改行ポイントのない文字列を折り返せないためです

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。