Justifying Text in PDF

Ok. I fixed the code and I could run it. The script justifies the text in the box on screen BUT when I try to use it in the PDF mode it won’t work. If you run it you’ll see a justify-aligned box of text (finally!) but the generated PDF will show a left-aligned text. Am I missing something?

The scripts:

justify.pde

import processing.core.PApplet;
import processing.awt.PGraphicsJava2D;
import processing.pdf.*;

PGraphics pdf;
PFont font, font2, font3;
int fontheight=26; 

void setup(){ 
  size(600,900, "PGraphicsJava2DJustify");
  background(255);
  fill(0); 
  
  String texto = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";  
    
  font =createFont("Georgia", fontheight);
  
  textAlign(PGraphicsJava2DJustify.JUSTIFY);
  textFont(font,fontheight); 
  text(texto, 40, 40, 500, 700);
  
  
  // pdf creation
  
  pdf = createGraphics(600,900, PDF, "pdf/justify.pdf"); 
  pdf.beginDraw();
  pdf.background(255);
  pdf.fill(0); 
    
  pdf.textAlign(PGraphicsJava2DJustify.JUSTIFY);
  pdf.textFont(font,fontheight); 
  pdf.text(texto,40,40, 500,700);
  
  pdf.dispose();
  pdf.endDraw();
}

PGraphicsJava2DJustify.java

import processing.core.PApplet;
import processing.awt.PGraphicsJava2D;

public class PGraphicsJava2DJustify extends PGraphicsJava2D {
  
  public static int JUSTIFY = 4;
  
  static protected void showTextFontException(String method) {
    throw new RuntimeException("Use textFont() before " + method + "()");
  }
  
  public void text(String str, float x1, float y1, float x2, float y2) {
    if (textFont == null) {
      showTextFontException("text");
    }

    if (textMode == SCREEN) loadPixels();

    float hradius, vradius;
    switch (rectMode) {
    case CORNER:
      x2 += x1; y2 += y1;
      break;
    case RADIUS:
      hradius = x2;
      vradius = y2;
      x2 = x1 + hradius;
      y2 = y1 + vradius;
      x1 -= hradius;
      y1 -= vradius;
      break;
    case CENTER:
      hradius = x2 / 2.0f;
      vradius = y2 / 2.0f;
      x2 = x1 + hradius;
      y2 = y1 + vradius;
      x1 -= hradius;
      y1 -= vradius;
    }
    if (x2 < x1) {
      float temp = x1; x1 = x2; x2 = temp;
    }
    if (y2 < y1) {
      float temp = y1; y1 = y2; y2 = temp;
    }

//    float currentY = y1;
    float boxWidth = x2 - x1;

//    // ala illustrator, the text itself must fit inside the box
//    currentY += textAscent(); //ascent() * textSize;
//    // if the box is already too small, tell em to f off
//    if (currentY > y2) return;

    float spaceWidth = textWidth(' ');

    if (textBreakStart == null) {
      textBreakStart = new int[20];
      textBreakStop = new int[20];
    }
    textBreakCount = 0;

    int length = str.length();
    if (length + 1 > textBuffer.length) {
      textBuffer = new char[length + 1];
    }
    str.getChars(0, length, textBuffer, 0);
    // add a fake newline to simplify calculations
    textBuffer[length++] = '\n';

    int sentenceStart = 0;
    for (int i = 0; i < length; i++) {
      if (textBuffer[i] == '\n') {
//        currentY = textSentence(textBuffer, sentenceStart, i,
//                                lineX, boxWidth, currentY, y2, spaceWidth);
        boolean legit =
          textSentence(textBuffer, sentenceStart, i, boxWidth, spaceWidth);
        if (!legit) break;
//      if (Float.isNaN(currentY)) break;  // word too big (or error)
//      if (currentY > y2) break;  // past the box
        sentenceStart = i + 1;
      }
    }

    // lineX is the position where the text starts, which is adjusted
    // to left/center/right based on the current textAlign
    float lineX = x1; //boxX1;
    if (textAlign == CENTER) {
      lineX = lineX + boxWidth/2f;
    } else if (textAlign == RIGHT) {
      lineX = x2; //boxX2;
    }

    float boxHeight = y2 - y1;
    //int lineFitCount = 1 + PApplet.floor((boxHeight - textAscent()) / textLeading);
    // incorporate textAscent() for the top (baseline will be y1 + ascent)
    // and textDescent() for the bottom, so that lower parts of letters aren't
    // outside the box. [0151]
    float topAndBottom = textAscent() + textDescent();
    int lineFitCount = 1 + PApplet.floor((boxHeight - topAndBottom) / textLeading);
    int lineCount = Math.min(textBreakCount, lineFitCount);

    if (textAlignY == CENTER) {
      float lineHigh = textAscent() + textLeading * (lineCount - 1);
      float y = y1 + textAscent() + (boxHeight - lineHigh) / 2;
      for (int i = 0; i < lineCount; i++) {
        textLineAlignImpl(textBuffer, textBreakStart[i], textBreakStop[i], lineX, y, boxWidth);
        y += textLeading;
      }

    } else if (textAlignY == BOTTOM) {
      float y = y2 - textDescent() - textLeading * (lineCount - 1);
      for (int i = 0; i < lineCount; i++) {
        textLineAlignImpl(textBuffer, textBreakStart[i], textBreakStop[i], lineX, y, boxWidth);
        y += textLeading;
      }

    } else {  // TOP or BASELINE just go to the default
      float y = y1 + textAscent();
      for (int i = 0; i < lineCount; i++) {
        textLineAlignImpl(textBuffer, textBreakStart[i], textBreakStop[i], lineX, y, boxWidth);
        y += textLeading;
      }
    }

    if (textMode == SCREEN) updatePixels();
  }

  //////////////////////////////////////////////////////////////

  // TEXT IMPL

  // These are most likely to be overridden by subclasses, since the other
  // (public) functions handle generic features like setting alignment.


  /**
   * Handles placement of a text line, then calls textLineImpl
   * to actually render at the specific point.
   */
  protected void textLineAlignImpl(char buffer[], int start, int stop,
                                   float x, float y, float boxWidth) {
    if (textAlign == CENTER) {
      x -= textWidthImpl(buffer, start, stop) / 2f;

    } else if (textAlign == RIGHT) {
      x -= textWidthImpl(buffer, start, stop);
    }

    textLineImpl(buffer, start, stop, x, y, boxWidth);
  }
  protected void textLineAlignImpl(char buffer[], int start, int stop,
                                   float x, float y) {
    textLineAlignImpl(buffer, start, stop, x, y, 0);
  }


  /**
   * Implementation of actual drawing for a line of text.
    */
  protected void textLineImpl(char buffer[], int start, int stop,
                              float x, float y) {
    textLineImpl(buffer, start, stop, x, y, 0);
  }
  protected void textLineImpl(char buffer[], int start, int stop,
                              float x, float y, float boxWidth) {
    
    int nbSpaces = 0, strWidth = 0, nbPixels = 0, j = 0;
    if (textAlign == JUSTIFY) {
      // remove last character if this is a space or similar
      if (stop > 0 && WHITESPACE.indexOf(buffer[stop-1]) > -1) stop--;
      // count number of spaces and total string width
      for (int index = start; index < stop; index++) {
        if (WHITESPACE.indexOf(buffer[index]) > -1) nbSpaces++;
        strWidth += textWidth(buffer[index]);
      }
      // count how many extra-pixels to add to reach boxWidth
      nbPixels = (int) boxWidth - strWidth;
      // distribute the extra pixels after each space
      int[] pixelsToAddAfterToken = new int[nbSpaces];
      for (int i = 0; i < nbSpaces; i++) {
        pixelsToAddAfterToken[i] = (int) (nbPixels/nbSpaces);
        if (i <= nbPixels%nbSpaces) pixelsToAddAfterToken[i]++;
      }
      
      for (int index = start; index < stop; index++) {
          textCharImpl(buffer[index], x, y);
          
          x += textWidth(buffer[index]);
          // if this is a space char, add the extra pixel for justification
          if (WHITESPACE.indexOf(buffer[index]) > -1) x += pixelsToAddAfterToken[j++];
          
      }
      
    }
    else {
        
        for (int index = start; index < stop; index++) {
          textCharImpl(buffer[index], x, y);

          // this doesn't account for kerning
          x += textWidth(buffer[index]);
          
        }
        
    }

  }

}

This is really urgent :frowning: I’ll appreciate any hint with this.

Thanks!

Did you change the code while posting it? I get “textX cannot be resolved to a variable”

Might want to delete those three lines.

I don’t know how the pdf renderer works, but I would guess that it writes output to PDF in parallel with output to the screen, not from the screen, and so it is ignoring your custom justify renderer.

If that is the case, here is my guess at how you could fix this:

  1. use the logic from your custom renderer to identify the insertion points for space.
  2. split your text lines into words, distribute the blank space into the word indexes, and then loop over the line and call text() on each word. Then the pdf renderer will pick that up because it is just normal text calls.

The other approach would be to try to override the pdf renderer, but I suspect (?) that will be much harder.

1 Like

Thanks for your message! It could be possible to apply the PDF method of text in the Java Justification class? Any hint about this option?

I don’t know, but if you want to change the PDF renderer, here it is:

I honestly think splitting on words and calling text multiple times would be easier. But if you can patch the PDF renderer to support justify, great! Here is textLineImpl:

Thanks for your help, Jeremy. I’m thinking this approach in the one in the second script I posted (in textLineImpl)? what I don’t get is how to redirect the output for the PDF renderer.