Package org.pdfclown.documents.contents.composition

Source Code of org.pdfclown.documents.contents.composition.BlockComposer$ContentPlaceholder

/*
  Copyright 2007-2010 Stefano Chizzolini. http://www.pdfclown.org

  Contributors:
    * Stefano Chizzolini (original code developer, http://www.stefanochizzolini.it)

  This file should be part of the source code distribution of "PDF Clown library"
  (the Program): see the accompanying README files for more info.

  This Program is free software; you can redistribute it and/or modify it under the terms
  of the GNU Lesser General Public License as published by the Free Software Foundation;
  either version 3 of the License, or (at your option) any later version.

  This Program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY,
  either expressed or implied; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE. See the License for more details.

  You should have received a copy of the GNU Lesser General Public License along with this
  Program (see README files); if not, go to the GNU website (http://www.gnu.org/licenses/).

  Redistribution and use, with or without modification, are permitted provided that such
  redistributions retain the above copyright notice, license and disclaimer, along with
  this list of conditions.
*/

package org.pdfclown.documents.contents.composition;

import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

import org.pdfclown.bytes.IOutputStream;
import org.pdfclown.documents.contents.ContentScanner;
import org.pdfclown.documents.contents.composition.Length.UnitModeEnum;
import org.pdfclown.documents.contents.fonts.Font;
import org.pdfclown.documents.contents.objects.ContainerObject;
import org.pdfclown.documents.contents.objects.ContentObject;
import org.pdfclown.documents.contents.objects.LocalGraphicsState;
import org.pdfclown.documents.contents.objects.ModifyCTM;
import org.pdfclown.documents.contents.objects.Operation;
import org.pdfclown.documents.contents.objects.SetWordSpace;

/**
  Content block composer.
  <p>It provides content positioning functionalities for page typesetting.</p>

  @author Stefano Chizzolini (http://www.stefanochizzolini.it)
  @since 0.0.3
  @version 0.1.0
*/
/*
  TODO: Manage all the graphics parameters (especially
  those text-related, like horizontal scaling etc.) using ContentScanner -- see PDF:1.6:5.2-3!!!
*/
public final class BlockComposer
{
  // <class>
  // <classes>
  private static final class ContentPlaceholder
    extends Operation
  {
    public List<ContentObject> objects = new ArrayList<ContentObject>();

    public ContentPlaceholder(
      )
    {super(null);}

    @SuppressWarnings("unused")
    public List<ContentObject> getObjects(
      )
    {return objects;}

    @Override
    public void writeTo(
      IOutputStream stream
      )
    {
      for(ContentObject object : objects)
      {object.writeTo(stream);}
    }
  }

  private static final class Row
  {
    /**
      Row's objects.
    */
    public ArrayList<RowObject> objects = new ArrayList<RowObject>();
    /**
      Number of space characters.
    */
    public int spaceCount = 0;
    /**
      Row's graphics objects container.
    */
    @SuppressWarnings("unused")
    public ContentPlaceholder container;

    public float height;
    /**
      Vertical location relative to the block frame.
    */
    public float y;
    public float width;

    Row(
      ContentPlaceholder container,
      float y
      )
    {
      this.container = container;
      this.y = y;
    }
  }

  private static final class RowObject
  {
    /**
      Row object's graphics objects container.
    */
    public ContainerObject container;

    public float height;
    @SuppressWarnings("unused")
    public float width;

    public int spaceCount;

    RowObject(
      ContainerObject container,
      float height,
      float width,
      int spaceCount
      )
    {
      this.container = container;
      this.height = height;
      this.width = width;
      this.spaceCount = spaceCount;
    }
  }
  // </classes>

  // <dynamic>
  /*
    NOTE: In order to provide fine-grained alignment,
    there are 2 postproduction state levels:
      1- row level (see endRow());
      2- block level (see end()).

    NOTE: Graphics instructions' layout follows this scheme (XS-BNF syntax):
      block = { beginLocalState translation parameters rows endLocalState }
      beginLocalState { "q\r" }
      translation = { "1 0 0 1 " number ' ' number "cm\r" }
      parameters = { ... } // Graphics state parameters.
      rows = { row* }
      row = { object* }
      object = { parameters beginLocalState translation content endLocalState }
      content = { ... } // Text, image (and so on) showing operators.
      endLocalState = { "Q\r" }
    NOTE: all the graphics state parameters within a block are block-level or row-object ones,
    i.e. they can't be represented inside row's local state, in order to allow parameter reuse
    within the same block.
  */
  // <fields>
  private PrimitiveComposer baseComposer;
  private ContentScanner scanner;

  private AlignmentXEnum alignmentX;
  private AlignmentYEnum alignmentY;
  private boolean hyphenation;
  private Length lineSpace = new Length(0, UnitModeEnum.Relative);

  /** Available area where to render the block contents inside. */
  private Rectangle2D frame;
  /** Actual area occupied by the block contents. */
  private Rectangle2D.Double boundBox;

  private Row currentRow;
  private boolean rowEnded;

  private LocalGraphicsState container;
  // </fields>

  // <constructors>
  public BlockComposer(
    PrimitiveComposer baseComposer
    )
  {
    this.baseComposer = baseComposer;
    this.scanner = baseComposer.getScanner();
  }
  // </constructors>

  // <interface>
  // <public>
  /**
    Begins a content block.
    @param frame Block boundaries.
    @param alignmentX Horizontal alignment.
    @param alignmentY Vertical alignment.
  */
  public void begin(
    Rectangle2D frame,
    AlignmentXEnum alignmentX,
    AlignmentYEnum alignmentY
    )
  {
    this.frame = frame;
    this.alignmentX = alignmentX;
    this.alignmentY = alignmentY;

    // Open the block local state!
    /*
      NOTE: This device allows a fine-grained control over the block representation.
      It MUST be coupled with a closing statement on block end.
    */
    container = baseComposer.beginLocalState();

    boundBox = new Rectangle2D.Double(
      frame.getX(),
      frame.getY(),
      frame.getWidth(),
      0
      );

    beginRow();
  }

  /**
    Ends the content block.
  */
  public void end(
    )
  {
    // End last row!
    endRow(true);

    // Block translation.
    container.getObjects().add(
      0,
      new ModifyCTM(
        1, 0, 0, 1,
        boundBox.x, // Horizontal translation.
        -boundBox.y // Vertical translation.
        )
      );

    // Close the block local state!
    baseComposer.end();
  }

  /**
    Gets the base composer.
  */
  public PrimitiveComposer getBaseComposer(
    )
  {return baseComposer;}

  /**
    Gets the actual area occupied by the block contents.
  */
  public Rectangle2D getBoundBox(
    )
  {return boundBox;}

  /**
    Gets the available area where to render the block contents inside.
  */
  public Rectangle2D getFrame(
    )
  {return frame;}

  /**
    Gets the text interline spacing.
  */
  public Length getLineSpace(
    )
  {return lineSpace;}

  /**
    Gets the content scanner.
  */
  public ContentScanner getScanner(
    )
  {return scanner;}

  /**
    Gets whether the hyphenation algorithm has to be applied.
  */
  public boolean isHyphenation(
    )
  {return hyphenation;}

  /**
    @see #isHyphenation()
  */
  public void setHyphenation(
    boolean value
    )
  {hyphenation = value;}

  /**
    @see #getLineSpace()
  */
  public void setLineSpace(
    Length value
    )
  {lineSpace = value;}

  /**
    Ends current paragraph.
  */
  public void showBreak(
    )
  {
    endRow(true);
    beginRow();
  }

  /**
    Ends current paragraph, specifying the offset of the next one.
    <h3>Remarks</h3>
    <p>This functionality allows higher-level features such as paragraph indentation and margin.</p>
    @param offset Relative location of the next paragraph.
  */
  public void showBreak(
    Dimension2D offset
    )
  {
    showBreak();

    currentRow.y += offset.getHeight();
    currentRow.width = (float)offset.getWidth();
  }

  /**
    Ends current paragraph, specifying the alignment of the next one.
    <h3>Remarks</h3>
    <p>This functionality allows higher-level features such as paragraph indentation and margin.</p>
    @param alignmentX Horizontal alignment.
  */
  public void showBreak(
    AlignmentXEnum alignmentX
    )
  {
    showBreak();

    this.alignmentX = alignmentX;
  }

  /**
    Ends current paragraph, specifying the offset and alignment of the next one.
    <h3>Remarks</h3>
    <p>This functionality allows higher-level features such as paragraph indentation and margin.</p>
    @param offset Relative location of the next paragraph.
    @param alignmentX Horizontal alignment.
  */
  public void showBreak(
    Dimension2D offset,
    AlignmentXEnum alignmentX
    )
  {
    showBreak(offset);

    this.alignmentX = alignmentX;
  }

  /**
    Shows text.
    @return Last shown character index.
  */
  public int showText(
    String text
    )
  {
    if(currentRow == null
      || text == null)
      return 0;

    ContentScanner.GraphicsState state = baseComposer.getState();
    Font font = state.getFont();
    float fontSize = state.getFontSize();
    float lineHeight = font.getLineHeight(fontSize);

    TextFitter textFitter = new TextFitter(
      text,
      0,
      font,
      fontSize,
      hyphenation
      );
    int textLength = text.length();
    int index = 0;

textShowing:
    while(true)
    {
      // Beginning of current row?
      if(currentRow.width == 0)
      {
        // Removing leading spaces...
        while(true)
        {
          // Did we reach the text end?
          if(index == textLength)
            break textShowing;

          if(text.charAt(index) != ' ')
            break;

          index++;
        }
      }

      // Text height: exceeds current row's height?
      if(lineHeight > currentRow.height)
      {
        // Text height: exceeds block's remaining vertical space?
        if(lineHeight > frame.getHeight() - currentRow.y) // Text exceeds.
        {
          // Terminate the current row!
          endRow(false);
          break textShowing;
        }
        else // Text doesn't exceed.
        {
          // Adapt current row's height!
          currentRow.height = lineHeight;
        }
      }

      // Does the text fit?
      if(textFitter.fit(
        index,
        (float)(frame.getWidth() - currentRow.width) // Remaining row width.
        ))
      {
        // Get the fitting text!
        String textChunk = textFitter.getFittedText();
        float textChunkWidth = textFitter.getFittedWidth();
        Point2D textChunkLocation = new Point2D.Double(
          currentRow.width,
          currentRow.y
          );

        // Render the fitting text:
        // - open the row object's local state!
        /*
          NOTE: This device allows a fine-grained control over the row object's representation.
          It MUST be coupled with a closing statement on row object's end.
        */
        RowObject object = new RowObject(
          baseComposer.beginLocalState(),
          lineHeight,
          textChunkWidth,
          countOccurrence(' ',textChunk)
          );
        currentRow.objects.add(object);
        currentRow.spaceCount += object.spaceCount;
        // - show the text chunk!
        baseComposer.showText(
          textChunk,
          textChunkLocation
          );
        // - close the row object's local state!
        baseComposer.end();

        // Update ancillary parameters:
        // - update row width!
        currentRow.width += textChunkWidth;
        // - update cursor position!
        index = textFitter.getEndIndex();
      }

      // Evaluating trailing text...
trailParsing:
      while(true)
      {
        // Did we reach the text end?
        if(index == textLength)
          break textShowing;

        switch(text.charAt(index))
        {
          case '\r':
            break;
          case '\n':
            // New paragraph!
            index++;
            showBreak();
            break trailParsing;
          default:
            // New row (within the same paragraph)!
            endRow(false); beginRow();
            break trailParsing;
        }

        index++;
      }
    }

    return index;
  }
  // </public>

  // <private>
  /**
    Begins a content row.
  */
  private void beginRow(
    )
  {
    rowEnded = false;

    float rowY = (float)boundBox.height;
    if(rowY > 0)
    {
      ContentScanner.GraphicsState state = baseComposer.getState();
      rowY += lineSpace.getValue(state.getFont().getLineHeight(state.getFontSize()));
    }
    currentRow = new Row(
      (ContentPlaceholder)baseComposer.add(new ContentPlaceholder()),
      rowY
      );
  }

  private int countOccurrence(
    char value,
    String text
    )
  {
    int count = 0;
    int fromIndex = 0;
    do
    {
      int foundIndex = text.indexOf(value,fromIndex);
      if(foundIndex == -1)
        return count;

      count++;

      fromIndex = foundIndex + 1;
    }
    while(true);
  }

  /**
    Ends the content row.
    @param broken Indicates whether this is the end of a paragraph.
  */
  private void endRow(
    boolean broken
    )
  {
    if(rowEnded)
      return;

    rowEnded = true;

    float[] objectXOffsets = new float[currentRow.objects.size()]; // Horizontal object displacements.
    float wordSpace = 0; // Exceeding space among words.
    float rowXOffset = 0; // Horizontal row offset.

    List<RowObject> objects = currentRow.objects;

    // Horizontal alignment.
    AlignmentXEnum alignmentX = this.alignmentX;
    switch(alignmentX)
    {
      case Left:
        break;
      case Right:
        rowXOffset = (float)frame.getWidth() - currentRow.width;
        break;
      case Center:
        rowXOffset = ((float)frame.getWidth() - currentRow.width) / 2;
        break;
      case Justify:
        // Are there NO spaces?
        if(currentRow.spaceCount == 0
          || broken) // NO spaces.
        {
          /* NOTE: This situation equals a simple left alignment. */
          alignmentX = AlignmentXEnum.Left;
        }
        else // Spaces exist.
        {
          // Calculate the exceeding spacing among the words!
          wordSpace = ((float)frame.getWidth() - currentRow.width) / currentRow.spaceCount;
          // Define the horizontal offsets for justified alignment.
          for(
            int index = 1,
              count = objects.size();
            index < count;
            index++
            )
          {
            /*
              NOTE: The offset represents the horizontal justification gap inserted
              at the left side of each object.
            */
            objectXOffsets[index] = objectXOffsets[index - 1] + objects.get(index - 1).spaceCount * wordSpace;
          }
        }
        break;
    }

    SetWordSpace wordSpaceOperation = new SetWordSpace(wordSpace);

    // Vertical alignment and translation.
    for(
      int index = objects.size() - 1;
      index >= 0;
      index--
      )
    {
      RowObject object = objects.get(index);

      // Vertical alignment.
      float objectYOffset = 0;
//TODO:IMPL image support!!!
//       switch(object.type)
//       {
//         case Text:
          objectYOffset = -(currentRow.height - object.height); // Linebase-anchored vertical alignment.
//           break;
//         case Image:
//           objectYOffset = -(currentRow.height - object.height) / 2; // Centered vertical alignment.
//           break;
//       }

      List<ContentObject> containedGraphics = object.container.getObjects();
      // Word spacing.
      containedGraphics.add(0,wordSpaceOperation);
      // Translation.
      containedGraphics.add(
        0,
        new ModifyCTM(
          1, 0, 0, 1,
          objectXOffsets[index] + rowXOffset, // Horizontal alignment.
          objectYOffset // Vertical alignment.
          )
        );
    }

    // Update the actual block height!
    boundBox.height = currentRow.y + currentRow.height;

    // Update the actual block vertical location!
    float xOffset;
    switch(alignmentY)
    {
      case Bottom:
        xOffset = (float)(frame.getHeight() - boundBox.height);
        break;
      case Middle:
        xOffset = (float)(frame.getHeight() - boundBox.height) / 2;
        break;
      case Top:
      default:
        xOffset = 0;
        break;
    }
    boundBox.y = frame.getY() + xOffset;

    // Discard the current row!
    currentRow = null;
  }
  // </private>
  // </interface>
  // </dynamic>
  // </class>
}
TOP

Related Classes of org.pdfclown.documents.contents.composition.BlockComposer$ContentPlaceholder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.