Package org.pdfclown.documents.contents.composition

Source Code of org.pdfclown.documents.contents.composition.PrimitiveComposer

/*
  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.Dimension;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;

import org.pdfclown.documents.Page;
import org.pdfclown.documents.contents.ContentScanner;
import org.pdfclown.documents.contents.FontResources;
import org.pdfclown.documents.contents.IContentContext;
import org.pdfclown.documents.contents.LineCapEnum;
import org.pdfclown.documents.contents.LineJoinEnum;
import org.pdfclown.documents.contents.Resources;
import org.pdfclown.documents.contents.TextRenderModeEnum;
import org.pdfclown.documents.contents.XObjectResources;
import org.pdfclown.documents.contents.colorSpaces.Color;
import org.pdfclown.documents.contents.colorSpaces.ColorSpace;
import org.pdfclown.documents.contents.colorSpaces.DeviceCMYKColorSpace;
import org.pdfclown.documents.contents.colorSpaces.DeviceGrayColorSpace;
import org.pdfclown.documents.contents.colorSpaces.DeviceRGBColorSpace;
import org.pdfclown.documents.contents.fonts.Font;
import org.pdfclown.documents.contents.objects.BeginMarkedContent;
import org.pdfclown.documents.contents.objects.BeginSubpath;
import org.pdfclown.documents.contents.objects.CloseSubpath;
import org.pdfclown.documents.contents.objects.CompositeObject;
import org.pdfclown.documents.contents.objects.ContentObject;
import org.pdfclown.documents.contents.objects.DrawCurve;
import org.pdfclown.documents.contents.objects.DrawLine;
import org.pdfclown.documents.contents.objects.DrawRectangle;
import org.pdfclown.documents.contents.objects.LocalGraphicsState;
import org.pdfclown.documents.contents.objects.MarkedContent;
import org.pdfclown.documents.contents.objects.ModifyCTM;
import org.pdfclown.documents.contents.objects.ModifyClipPath;
import org.pdfclown.documents.contents.objects.PaintPath;
import org.pdfclown.documents.contents.objects.PaintXObject;
import org.pdfclown.documents.contents.objects.SetCharSpace;
import org.pdfclown.documents.contents.objects.SetFillColor;
import org.pdfclown.documents.contents.objects.SetFillColorSpace;
import org.pdfclown.documents.contents.objects.SetFont;
import org.pdfclown.documents.contents.objects.SetLineCap;
import org.pdfclown.documents.contents.objects.SetLineDash;
import org.pdfclown.documents.contents.objects.SetLineJoin;
import org.pdfclown.documents.contents.objects.SetLineWidth;
import org.pdfclown.documents.contents.objects.SetMiterLimit;
import org.pdfclown.documents.contents.objects.SetStrokeColor;
import org.pdfclown.documents.contents.objects.SetStrokeColorSpace;
import org.pdfclown.documents.contents.objects.SetTextLead;
import org.pdfclown.documents.contents.objects.SetTextMatrix;
import org.pdfclown.documents.contents.objects.SetTextRenderMode;
import org.pdfclown.documents.contents.objects.SetTextRise;
import org.pdfclown.documents.contents.objects.SetTextScale;
import org.pdfclown.documents.contents.objects.SetWordSpace;
import org.pdfclown.documents.contents.objects.ShowSimpleText;
import org.pdfclown.documents.contents.objects.Text;
import org.pdfclown.documents.contents.objects.TranslateTextRelative;
import org.pdfclown.documents.contents.objects.TranslateTextToNextLine;
import org.pdfclown.documents.contents.xObjects.XObject;
import org.pdfclown.documents.interaction.actions.Action;
import org.pdfclown.documents.interaction.annotations.Link;
import org.pdfclown.objects.PdfName;
import org.pdfclown.util.NotImplementedException;

/**
  Content stream primitive composer.
  <p>It provides the basic (primitive) operations described by the PDF specification
  for graphics content composition.</p>
  <h3>Remarks</h3>
  <p>This class leverages the object-oriented content stream modelling infrastructure,
  which encompasses 1st-level content stream objects (operations),
  2nd-level content stream objects (graphics objects) and full graphics state support.</p>

  @author Stefano Chizzolini (http://www.stefanochizzolini.it)
  @since 0.0.4
  @version 0.1.0
*/
public final class PrimitiveComposer
{
  // <class>
  // <dynamic>
  // <fields>
  private ContentScanner scanner;
  // </fields>

  // <constructors>
  public PrimitiveComposer(
    ContentScanner scanner
    )
  {setScanner(scanner);}

  public PrimitiveComposer(
    IContentContext context
    )
  {
    this(
      new ContentScanner(context.getContents())
      );
  }
  // </constructors>

  // <interface>
  // <public>
  /**
    Adds a content object.

    @return The added content object.
  */
  public ContentObject add(
    ContentObject object
    )
  {
    scanner.insert(object);
    scanner.moveNext();

    return object;
  }

  /**
    Applies a transformation to the coordinate system from user space to device space [PDF:1.6:4.3.3].
    <h3>Remarks</h3>
    <p>The transformation is applied to the current transformation matrix (CTM) by concatenation,
    i.e. it doesn't replace it.</p>

    @see #setMatrix(float,float,float,float,float,float)

    @param a Item 0,0 of the matrix.
    @param b Item 0,1 of the matrix.
    @param c Item 1,0 of the matrix.
    @param d Item 1,1 of the matrix.
    @param e Item 2,0 of the matrix.
    @param f Item 2,1 of the matrix.
  */
  public void applyMatrix(
    double a,
    double b,
    double c,
    double d,
    double e,
    double f
    )
  {add(new ModifyCTM(a,b,c,d,e,f));}

  /**
    Adds a composite object beginning it.

    @see #end()

    @return The added composite object.
  */
  public CompositeObject begin(
    CompositeObject object
    )
  {
    // Insert the new object at the current level!
    scanner.insert(object);
    // The new object's children level is the new current level!
    scanner = scanner.getChildLevel();

    return object;
  }

  /**
    Begins a new nested graphics state context [PDF:1.6:4.3.1].

    @see #end()

    @return The added local graphics state object.
  */
  public LocalGraphicsState beginLocalState(
    )
  {return (LocalGraphicsState)begin(new LocalGraphicsState());}

  /**
    Begins a new marked-content sequence [PDF:1.6:10.5].

    @see #end()

    @return The added marked-content sequence.
  */
  public MarkedContent beginMarkedContent(
    PdfName tag
    )
  {
    return (MarkedContent)begin(
      new MarkedContent(
        new BeginMarkedContent(tag)
        )
      );
  }

  /**
    Modifies the current clipping path by intersecting it with the current path [PDF:1.6:4.4.1].
    <h3>Remarks</h3>
    <p>It can be validly called only just before painting the current path.</p>
  */
  public void clip(
    )
  {
    add(ModifyClipPath.NonZero);
    add(PaintPath.EndPathNoOp);
  }

  /**
    Closes the current subpath by appending a straight line segment
    from the current point to the starting point of the subpath [PDF:1.6:4.4.1].
  */
  public void closePath(
    )
  {add(CloseSubpath.Value);}

  /**
    Draws a circular arc.

    @see #stroke()
    @since 0.0.7

    @param location Arc location.
    @param startAngle Starting angle.
    @param endAngle Ending angle.
  */
  public void drawArc(
    RectangularShape location,
    float startAngle,
    float endAngle
    )
  {drawArc(location,startAngle,endAngle,0,1);}

  /**
    Draws an arc.

    @see #stroke()
    @since 0.0.7

    @param location Arc location.
    @param startAngle Starting angle.
    @param endAngle Ending angle.
    @param branchWidth Distance between the spiral branches. '0' value degrades to a circular arc.
    @param branchRatio Linear coefficient applied to the branch width. '1' value degrades to a constant branch width.
  */
  public void drawArc(
    RectangularShape location,
    float startAngle,
    float endAngle,
    float branchWidth,
    float branchRatio
    )
  {drawArc(location,startAngle,endAngle,branchWidth,branchRatio,true);}

  /**
    Draws a cubic Bezier curve from the current point [PDF:1.6:4.4.1].

    @see #stroke()
    @since 0.0.7

    @param endPoint Ending point.
    @param startControl Starting control point.
    @param endControl Ending control point.
  */
  public void drawCurve(
    Point2D endPoint,
    Point2D startControl,
    Point2D endControl
    )
  {
    float contextHeight = (float)scanner.getContentContext().getBox().getHeight();
    add(
      new DrawCurve(
        endPoint.getX(),
        contextHeight - endPoint.getY(),
        startControl.getX(),
        contextHeight - startControl.getY(),
        endControl.getX(),
        contextHeight - endControl.getY()
        )
      );
  }

  /**
    Draws a cubic Bezier curve [PDF:1.6:4.4.1].

    @see #stroke()
    @since 0.0.7

    @param startPoint Starting point.
    @param endPoint Ending point.
    @param startControl Starting control point.
    @param endControl Ending control point.
  */
  public void drawCurve(
    Point2D startPoint,
    Point2D endPoint,
    Point2D startControl,
    Point2D endControl
    )
  {
    beginSubpath(startPoint);
    drawCurve(endPoint,startControl,endControl);
  }

  /**
    Draws an ellipse.
    @since 0.0.7

    @see #fill()
    @see #fillStroke()
    @see #stroke()

    @param location Ellipse location.
  */
  public void drawEllipse(
    RectangularShape location
    )
  {drawArc(location,0,360);}

  /**
    Draws a line from the current point [PDF:1.6:4.4.1].

    @see #stroke()
    @since 0.0.7

    @param endPoint Ending point.
  */
  public void drawLine(
    Point2D endPoint
    )
  {
    add(
      new DrawLine(
        endPoint.getX(),
        scanner.getContentContext().getBox().getHeight() - endPoint.getY()
        )
      );
  }

  /**
    Draws a line [PDF:1.6:4.4.1].

    @see #stroke()
    @since 0.0.7

    @param startPoint Starting point.
    @param endPoint Ending point.
  */
  public void drawLine(
    Point2D startPoint,
    Point2D endPoint
    )
  {
    beginSubpath(startPoint);
    drawLine(endPoint);
  }

  /**
    Draws a polygon.
    <h3>Remarks</h3>
    <p>A polygon is the same as a multiple line except that it's a closed path.</p>

    @see #fill()
    @see #fillStroke()
    @see #stroke()
    @since 0.0.7

    @param points Points.
  */
  public void drawPolygon(
    Point2D[] points
    )
  {
    drawPolyline(points);
    closePath();
  }

  /**
    Draws a multiple line.

    @see #stroke()
    @since 0.0.7

    @param points Points.
  */
  public void drawPolyline(
    Point2D[] points
    )
  {
    beginSubpath(points[0]);
    for(
      int index = 1,
        length = points.length;
      index < length;
      index++
      )
    {drawLine(points[index]);}
  }

  /**
    Draws a rectangle [PDF:1.6:4.4.1].

    @see #fill()
    @see #fillStroke()
    @see #stroke()

    @param location Rectangle location.
  */
  public void drawRectangle(
    RectangularShape location
    )
  {drawRectangle(location,0);}

  /**
    Draws a rounded rectangle.

    @see #fill()
    @see #fillStroke()
    @see #stroke()

    @param location Rectangle location.
    @param radius Vertex radius, '0' value degrades to squared vertices.
  */
  public void drawRectangle(
    RectangularShape location,
    float radius
    )
  {
    if(radius == 0)
    {
      add(
        new DrawRectangle(
          location.getX(),
          scanner.getContentContext().getBox().getHeight() - location.getY() - location.getHeight(),
          location.getWidth(),
          location.getHeight()
          )
        );
    }
    else
    {
      final double endRadians = Math.PI * 2;
      final double quadrantRadians = Math.PI / 2;
      double radians = 0;
      while(radians < endRadians)
      {
        double radians2 = radians + quadrantRadians;
        int sin2 = (int)Math.sin(radians2);
        int cos2 = (int)Math.cos(radians2);
        double x1 = 0, x2 = 0, y1 = 0, y2 = 0;
        float xArc = 0, yArc = 0;
        if(cos2 == 0)
        {
          if(sin2 == 1)
          {
            x1 = x2 = location.getX() + location.getWidth();
            y1 = location.getY() + location.getHeight() - radius;
            y2 = location.getY() + radius;

            xArc =- radius * 2;
            yArc =- radius;

            beginSubpath(new Point2D.Double(x1,y1));
          }
          else
          {
            x1 = x2 = location.getX();
            y1 = location.getY() + radius;
            y2 = location.getY() + location.getHeight() - radius;

            yArc =- radius;
          }
        }
        else if(cos2 == 1)
        {
          x1 = location.getX() + radius;
          x2 = location.getX() + location.getWidth() - radius;
          y1 = y2 = location.getY() + location.getHeight();

          xArc =- radius;
          yArc =- radius*2;
        }
        else if(cos2 == -1)
        {
          x1 = location.getX() + location.getWidth() - radius;
          x2 = location.getX() + radius;
          y1 = y2 = location.getY();

          xArc=-radius;
        }
        drawLine(
          new Point2D.Double(x2,y2)
          );
        drawArc(
          new Rectangle2D.Double(x2+xArc, y2+yArc, radius*2, radius*2),
          (float)Math.toDegrees(radians),
          (float)Math.toDegrees(radians2),
          0,
          1,
          false
          );

        radians = radians2;
      }
    }
  }

  /**
    Draws a spiral.

    @see #stroke()
    @since 0.0.7

    @param center Spiral center.
    @param startAngle Starting angle.
    @param endAngle Ending angle.
    @param branchWidth Distance between the spiral branches.
    @param branchRatio Linear coefficient applied to the branch width.
  */
  public void drawSpiral(
    Point2D center,
    float startAngle,
    float endAngle,
    float branchWidth,
    float branchRatio
    )
  {
    drawArc(
      new Rectangle2D.Double(center.getX(),center.getY(),0.0001,0.0001),
      startAngle,
      endAngle,
      branchWidth,
      branchRatio
      );
  }

  /**
    Ends the current (innermostly-nested) composite object.

    @see #begin(CompositeObject)
  */
  public void end(
    )
  {
    scanner = scanner.getParentLevel();
    scanner.moveNext();
  }

  /**
    Fills the path using the current color [PDF:1.6:4.4.2].

    @see #setFillColor(Color)
  */
  public void fill(
    )
  {add(PaintPath.Fill);}

  /**
    Fills and then strokes the path using the current colors [PDF:1.6:4.4.2].

    @see #setFillColor(Color)
    @see #setStrokeColor(Color)
  */
  public void fillStroke(
    )
  {add(PaintPath.FillStroke);}

  /**
    Serializes the contents into the content stream.
  */
  public void flush(
    )
  {scanner.getContents().flush();}

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

  /**
    Gets the current graphics state [PDF:1.6:4.3].
  */
  public ContentScanner.GraphicsState getState(
    )
  {return scanner.getState();}

  /**
    Applies a rotation to the coordinate system from user space to device space [PDF:1.6:4.2.2].

    @see #applyMatrix(float,float,float,float,float,float)

    @param angle Rotational counterclockwise angle.
  */
  public void rotate(
    float angle
    )
  {
    double rad = angle * Math.PI / 180;
    double cos = Math.cos(rad);
    double sin = Math.sin(rad);
    applyMatrix(cos, sin, -sin, cos, 0, 0);
  }

  /**
    Applies a rotation to the coordinate system from user space to device space [PDF:1.6:4.2.2].

    @see #applyMatrix(float,float,float,float,float,float)

    @param angle Rotational counterclockwise angle.
    @param origin Rotational pivot point; it becomes the new coordinates origin.
  */
  public void rotate(
    float angle,
    Point2D origin
    )
  {
    // Center to the new origin!
    translate(
      (float)origin.getX(),
      (float)(scanner.getContentContext().getBox().getHeight() - origin.getY())
      );
    // Rotate on the new origin!
    rotate(angle);
    // Restore the standard vertical coordinates system!
    translate(
      0,
      (float)-scanner.getContentContext().getBox().getHeight()
      );
  }

  /**
    Applies a scaling to the coordinate system from user space to device space [PDF:1.6:4.2.2].

    @see #applyMatrix(float,float,float,float,float,float)

    @param ratioX Horizontal scaling ratio.
    @param ratioY Vertical scaling ratio.
  */
  public void scale(
    float ratioX,
    float ratioY
    )
  {applyMatrix(ratioX, 0, 0, ratioY, 0, 0);}

  /**
    Sets the character spacing parameter [PDF:1.6:5.2.1].
  */
  public void setCharSpace(
    float value
    )
  {add(new SetCharSpace(value));}

  /**
    Sets the nonstroking color value [PDF:1.6:4.5.7].

    @see #setStrokeColor(Color)
  */
  public void setFillColor(
    Color<?> value
    )
  {
    if(!scanner.getState().getFillColorSpace().equals(value.getColorSpace()))
    {
      // Set filling color space!
      add(
        new SetFillColorSpace(
          getColorSpaceName(
            value.getColorSpace()
            )
          )
        );
    }

    add(new SetFillColor(value));
  }

  /**
    Sets the font [PDF:1.6:5.2].

    @param name Resource identifier of the font.
    @param size Scaling factor (points).
  */
  public void setFont(
    PdfName name,
    float size
    )
  {
    // Doesn't the font exist in the context resources?
    if(!scanner.getContentContext().getResources().getFonts().containsKey(name))
      throw new IllegalArgumentException("No font resource associated to the given argument (name:'name'; value:'" + name + "';)");

    add(new SetFont(name,size));
  }

  /**
    Sets the font [PDF:1.6:5.2].
    <h3>Remarks</h3>
    <p>The <code>value</code> is checked for presence in the current resource
    dictionary: if it isn't available, it's automatically added. If you need to
    avoid such a behavior, use {@link #setFont(PdfName,float) setFont(PdfName,float)}.</p>

    @param value Font.
    @param size Scaling factor (points).
  */
  public void setFont(
    Font value,
    float size
    )
  {setFont(getFontName(value),size);}

  /**
    Sets the text horizontal scaling [PDF:1.6:5.2.3].
  */
  public void setTextScale(
    float value
    )
  {add(new SetTextScale(value));}

  /**
    Sets the text leading [PDF:1.6:5.2.4].
  */
  public void setTextLead(
    float value
    )
  {add(new SetTextLead(value));}

  /**
    Sets the line cap style [PDF:1.6:4.3.2].
  */
  public void setLineCap(
    LineCapEnum value
    )
  {add(new SetLineCap(value));}

  /**
    Sets the line dash pattern [PDF:1.6:4.3.2].

    @param phase Distance into the dash pattern at which to start the dash.
    @param unitsOn Length of evenly alternating dashes and gaps.
  */
  public void setLineDash(
    int phase,
    int unitsOn
    )
  {setLineDash(phase,unitsOn,unitsOn);}

  /**
    Sets the line dash pattern [PDF:1.6:4.3.2].

    @param phase Distance into the dash pattern at which to start the dash.
    @param unitsOn Length of dashes.
    @param unitsOff Length of gaps.
  */
  public void setLineDash(
    int phase,
    int unitsOn,
    int unitsOff
    )
  {add(new SetLineDash(phase,unitsOn,unitsOff));}

  /**
    Sets the line join style [PDF:1.6:4.3.2].
  */
  public void setLineJoin(
    LineJoinEnum value
    )
  {add(new SetLineJoin(value));}

  /**
    Sets the line width [PDF:1.6:4.3.2].
  */
  public void setLineWidth(
    float value
    )
  {add(new SetLineWidth(value));}

  /**
    Sets the transformation of the coordinate system from user space to device space [PDF:1.6:4.3.3].
    <h3>Remarks</h3>
    <p>The transformation replaces the current transformation matrix (CTM).</p>

    @see #applyMatrix(float,float,float,float,float,float)

    @param a Item 0,0 of the matrix.
    @param b Item 0,1 of the matrix.
    @param c Item 1,0 of the matrix.
    @param d Item 1,1 of the matrix.
    @param e Item 2,0 of the matrix.
    @param f Item 2,1 of the matrix.
  */
  public void setMatrix(
    float a,
    float b,
    float c,
    float d,
    float e,
    float f
    )
  {
    // Reset the CTM!
    add(ModifyCTM.getResetCTM(scanner.getState()));
    // Apply the transformation!
    add(new ModifyCTM(a,b,c,d,e,f));
  }

  /**
    Sets the miter limit [PDF:1.6:4.3.2].
  */
  public void setMiterLimit(
    float value
    )
  {add(new SetMiterLimit(value));}

  /**
    @see #getScanner()
  */
  public void setScanner(
    ContentScanner value
    )
  {scanner = value;}

  /**
    Sets the stroking color value [PDF:1.6:4.5.7].

    @see #setFillColor(Color)
  */
  public void setStrokeColor(
    Color<?> value
    )
  {
    if(!scanner.getState().getStrokeColorSpace().equals(value.getColorSpace()))
    {
      // Set stroking color space!
      add(
        new SetStrokeColorSpace(
          getColorSpaceName(
            value.getColorSpace()
            )
          )
        );
    }

    add(new SetStrokeColor(value));
  }

  /**
    Sets the text rendering mode [PDF:1.6:5.2.5].
  */
  public void setTextRenderMode(
    TextRenderModeEnum value
    )
  {add(new SetTextRenderMode(value));}

  /**
    Sets the text rise [PDF:1.6:5.2.6].
  */
  public void setTextRise(
    float value
    )
  {add(new SetTextRise(value));}

  /**
    Sets the word spacing [PDF:1.6:5.2.2].
  */
  public void setWordSpace(
    float value
    )
  {add(new SetWordSpace(value));}

  /**
    Shows the specified text on the page at the current location [PDF:1.6:5.3.2].

    @param value Text to show.
    @return Bounding box vertices in default user space units.
  */
  public Point2D[] showText(
    String value
    )
  {
    return showText(
      value,
      new Point2D.Double(0,0)
      );
  }

  /**
    Shows the link associated to the specified text on the page at the current location.

    @since 0.0.7

    @param value Text to show.
    @param action Action to apply when the link is activated.
    @return Link.
  */
  public Link showText(
    String value,
    Action action
    )
  {
    return showText(
      value,
      new Point2D.Double(0,0),
      action
      );
  }

  /**
    Shows the specified text on the page at the specified location [PDF:1.6:5.3.2].

    @param value Text to show.
    @param location Position at which showing the text.
    @return Bounding box vertices in default user space units.
  */
  public Point2D[] showText(
    String value,
    Point2D location
    )
  {
    return showText(
      value,
      location,
      AlignmentXEnum.Left,
      AlignmentYEnum.Top,
      0
      );
  }

  /**
    Shows the link associated to the specified text on the page at the specified location.

    @since 0.0.7

    @param value Text to show.
    @param location Position at which showing the text.
    @param action Action to apply when the link is activated.
    @return Link.
  */
  public Link showText(
    String value,
    Point2D location,
    Action action
    )
  {
    return showText(
      value,
      location,
      AlignmentXEnum.Left,
      AlignmentYEnum.Top,
      0,
      action
      );
  }

  /**
    Shows the specified text on the page at the specified location [PDF:1.6:5.3.2].

    @param value Text to show.
    @param location Anchor position at which showing the text.
    @param alignmentX Horizontal alignment.
    @param alignmentY Vertical alignment.
    @param rotation Rotational counterclockwise angle.
    @return Bounding box vertices in default user space units.
  */
  public Point2D[] showText(
    String value,
    Point2D location,
    AlignmentXEnum alignmentX,
    AlignmentYEnum alignmentY,
    float rotation
    )
  {
    ContentScanner.GraphicsState state = scanner.getState();
    Font font = state.getFont();
    float fontSize = state.getFontSize();
    float x = (float)location.getX();
    float y = (float)location.getY();
    float width = font.getKernedWidth(value,fontSize);
    float height = font.getLineHeight(fontSize);
    float descent = font.getDescent(fontSize);

    Point2D[] frame = new Point2D[4];

    if(alignmentX == AlignmentXEnum.Left
      && alignmentY == AlignmentYEnum.Top)
    {
      beginText();
      try
      {
        if(rotation == 0)
        {
          translateText(
            x,
            (float)(scanner.getContentContext().getBox().getHeight() - y - font.getAscent(fontSize))
            );
        }
        else
        {
          double rad = rotation * Math.PI / 180.0;
          double cos = Math.cos(rad);
          double sin = Math.sin(rad);

          setTextMatrix(
            cos,
            sin,
            -sin,
            cos,
            x,
            scanner.getContentContext().getBox().getHeight() - y - font.getAscent(fontSize)
            );
        }

        state = scanner.getState();
        frame[0] = state.textToDeviceSpace(new Point2D.Double(0,descent),true);
        frame[1] = state.textToDeviceSpace(new Point2D.Double(width,descent),true);
        frame[2] = state.textToDeviceSpace(new Point2D.Double(width,height+descent),true);
        frame[3] = state.textToDeviceSpace(new Point2D.Double(0,height+descent),true);

        // Add the text!
        add(new ShowSimpleText(font.encode(value)));
      }
      catch(Exception e)
      {throw new RuntimeException("Failed to show text.", e);}
      finally
      {end(); /* Ends the text object. */}
    }
    else
    {
      beginLocalState();
      try
      {
        // Coordinates transformation.
        double cos, sin;
        if(rotation == 0)
        {
          cos = 1;
          sin = 0;
        }
        else
        {
          double rad = rotation * Math.PI / 180.0;
          cos = Math.cos(rad);
          sin = Math.sin(rad);
        }
        // Apply the transformation!
        applyMatrix(
          cos,
          sin,
          -sin,
          cos,
          x,
          scanner.getContentContext().getBox().getHeight() - y
          );

        beginText();
        try
        {
          // Text coordinates adjustment.
          switch(alignmentX)
          {
            case Left:
              x = 0;
              break;
            case Right:
              x = -width;
              break;
            case Center:
            case Justify:
              x = -width / 2;
              break;
          }
          switch(alignmentY)
          {
            case Top:
              y = -font.getAscent(fontSize);
              break;
            case Bottom:
              y = height - font.getAscent(fontSize);
              break;
            case Middle:
              y = height / 2 - font.getAscent(fontSize);
              break;
          }
          // Apply the text coordinates adjustment!
          translateText(x,y);

          state = scanner.getState();
          frame[0] = state.textToDeviceSpace(new Point2D.Double(0,descent),true);
          frame[1] = state.textToDeviceSpace(new Point2D.Double(width,descent),true);
          frame[2] = state.textToDeviceSpace(new Point2D.Double(width,height+descent),true);
          frame[3] = state.textToDeviceSpace(new Point2D.Double(0,height+descent),true);

          // Add the text!
          add(new ShowSimpleText(font.encode(value)));
        }
        catch(Exception e)
        {throw new RuntimeException("Failed to show text.", e);}
        finally
        {end(); /* Ends the text object. */}
      }
      catch(Exception e)
      {throw new RuntimeException("Failed to show text.", e);}
      finally
      {end(); /* Ends the local state. */}
    }

    return frame;
  }

  /**
    Shows the link associated to the specified text on the page at the specified location.

    @since 0.0.7

    @param value Text to show.
    @param location Anchor position at which showing the text.
    @param alignmentX Horizontal alignment.
    @param alignmentY Vertical alignment.
    @param rotation Rotational counterclockwise angle.
    @param action Action to apply when the link is activated.
    @return Link.
  */
  public Link showText(
    String value,
    Point2D location,
    AlignmentXEnum alignmentX,
    AlignmentYEnum alignmentY,
    float rotation,
    Action action
    )
  {
    Point2D[] textFrame = showText(
      value,
      location,
      alignmentX,
      alignmentY,
      rotation
      );

    IContentContext contentContext = scanner.getContentContext();
    if(!(contentContext instanceof Page))
      throw new RuntimeException("Link can be shown only on page contexts.");

    Page page = (Page)contentContext;
    Rectangle2D linkBox = new Rectangle2D.Double(textFrame[0].getX(),textFrame[0].getY(),0,0);
    for(
      int index = 1,
        length = textFrame.length;
      index < length;
      index++
      )
    {linkBox.add(textFrame[index]);}

    return new Link(
      page,
      linkBox,
      action
      );
  }

  /**
    Shows the specified external object [PDF:1.6:4.7].

    @param name Resource identifier of the external object.
  */
  public void showXObject(
    PdfName name
    )
  {add(new PaintXObject(name));}

  /**
    Shows the specified external object [PDF:1.6:4.7].
    <h3>Remarks</h3>
    <p>The <code>value</code> is checked for presence in the current resource
    dictionary: if it isn't available, it's automatically added. If you need to
    avoid such a behavior, use {@link #showXObject(PdfName) #showXObject(PdfName)}.</p>

    @since 0.0.5

    @param value External object.
  */
  public void showXObject(
    XObject value
    )
  {showXObject(getXObjectName(value));}

  /**
    Shows the specified external object at the specified position [PDF:1.6:4.7].

    @param name Resource identifier of the external object.
    @param location Position at which showing the external object.
  */
  public void showXObject(
    PdfName name,
    Point2D location
    )
  {
    showXObject(
      name,
      location,
      new Dimension(0,0)
      );
  }

  /**
    Shows the specified external object at the specified position [PDF:1.6:4.7].
    <h3>Remarks</h3>
    <p>The <code>value</code> is checked for presence in the current resource
    dictionary: if it isn't available, it's automatically added. If you need to
    avoid such a behavior, use {@link #showXObject(PdfName,Point2D) #showXObject(PdfName,Point2D)}.</p>

    @since 0.0.5

    @param value External object.
    @param location Position at which showing the external object.
  */
  public void showXObject(
    XObject value,
    Point2D location
    )
  {
    showXObject(
      getXObjectName(value),
      location
      );
  }

  /**
    Shows the specified external object at the specified position [PDF:1.6:4.7].

    @since 0.0.5

    @param name Resource identifier of the external object.
    @param location Position at which showing the external object.
    @param size Size of the external object.
  */
  public void showXObject(
    PdfName name,
    Point2D location,
    Dimension2D size
    )
  {
    showXObject(
      name,
      location,
      size,
      AlignmentXEnum.Left,
      AlignmentYEnum.Top,
      0
      );
  }

  /**
    Shows the specified external object at the specified position [PDF:1.6:4.7].
    <h3>Remarks</h3>
    <p>The <code>value</code> is checked for presence in the current resource
    dictionary: if it isn't available, it's automatically added. If you need to
    avoid such a behavior, use {@link #showXObject(PdfName,Point2D,Dimension2D) #showXObject(PdfName,Point2D,Dimension2D)}.</p>

    @since 0.0.5

    @param value External object.
    @param location Position at which showing the external object.
    @param size Size of the external object.
  */
  public void showXObject(
    XObject value,
    Point2D location,
    Dimension2D size
    )
  {
    showXObject(
      getXObjectName(value),
      location,
      size
      );
  }

  /**
    Shows the specified external object at the specified position [PDF:1.6:4.7].

    @param name Resource identifier of the external object.
    @param location Position at which showing the external object.
    @param size Size of the external object.
    @param alignmentX Horizontal alignment.
    @param alignmentY Vertical alignment.
    @param rotation Rotational counterclockwise angle.
  */
  public void showXObject(
    PdfName name,
    Point2D location,
    Dimension2D size,
    AlignmentXEnum alignmentX,
    AlignmentYEnum alignmentY,
    float rotation
    )
  {
    XObject xObject = scanner.getContentContext().getResources().getXObjects().get(name);

    // Adjusting default dimensions...
    /*
      NOTE: Zero-valued dimensions represent default proportional dimensions.
    */
    Dimension2D xObjectSize = xObject.getSize();
    if(size.getWidth() == 0)
    {
      if(size.getHeight() == 0)
      {size.setSize(xObjectSize);}
      else
      {size.setSize(size.getHeight() * xObjectSize.getWidth() / xObjectSize.getHeight(),size.getHeight());}
    }
    else if(size.getHeight() == 0)
    {size.setSize(size.getWidth(),size.getWidth() * xObjectSize.getHeight() / xObjectSize.getWidth());}

    // Scaling.
    double[] matrix = xObject.getMatrix();
    double scaleX, scaleY;
    scaleX = size.getWidth() / (xObjectSize.getWidth() * matrix[0]);
    scaleY = size.getHeight() / (xObjectSize.getHeight() * matrix[3]);

    // Alignment.
    float locationOffsetX, locationOffsetY;
    switch(alignmentX)
    {
      case Left: locationOffsetX = 0; break;
      case Right: locationOffsetX = (float)size.getWidth(); break;
      case Center:
      case Justify:
      default: locationOffsetX = (float)size.getWidth() / 2; break;
    }
    switch(alignmentY)
    {
      case Top: locationOffsetY = (float)size.getHeight(); break;
      case Bottom: locationOffsetY = 0; break;
      case Middle:
      default: locationOffsetY = (float)size.getHeight() / 2; break;
    }

    beginLocalState();
    try
    {
      translate(
        (float)location.getX(),
        (float)(scanner.getContentContext().getBox().getHeight() - location.getY())
        );
      if(rotation != 0)
      {rotate(rotation);}
      applyMatrix(
        scaleX, 0, 0,
        scaleY,
        -locationOffsetX,
        -locationOffsetY
        );
      showXObject(name);
    }
    catch (Exception e)
    {throw new RuntimeException("Failed to show the xobject.",e);}
    finally
    {end(); /* Ends the local state. */}
  }

  /**
    Shows the specified external object at the specified position [PDF:1.6:4.7].
    <h3>Remarks</h3>
    <p>The <code>value</code> is checked for presence in the current resource
    dictionary: if it isn't available, it's automatically added. If you need to
    avoid such a behavior, use {@link #showXObject(PdfName,Point2D,Dimension2D,AlignmentXEnum,AlignmentYEnum,float) #showXObject(PdfName,Point2D,Dimension2D,AlignmentXEnum,AlignmentYEnum,float)}.</p>

    @since 0.0.5

    @param value External object.
    @param location Position at which showing the external object.
    @param size Size of the external object.
    @param alignmentX Horizontal alignment.
    @param alignmentY Vertical alignment.
    @param rotation Rotational counterclockwise angle.
  */
  public void showXObject(
    XObject value,
    Point2D location,
    Dimension2D size,
    AlignmentXEnum alignmentX,
    AlignmentYEnum alignmentY,
    float rotation
    )
  {
    showXObject(
      getXObjectName(value),
      location,
      size,
      alignmentX,
      alignmentY,
      rotation
      );
  }

  /**
    Strokes the path using the current color [PDF:1.6:4.4.2].

    @see #setStrokeColor(Color)
  */
  public void stroke(
    )
  {add(PaintPath.Stroke);}

  /**
    Applies a translation to the coordinate system from user space
    to device space [PDF:1.6:4.2.2].

    @see #applyMatrix(float,float,float,float,float,float)

    @param distanceX Horizontal distance.
    @param distanceY Vertical distance.
  */
  public void translate(
    float distanceX,
    float distanceY
    )
  {applyMatrix(1, 0, 0, 1, distanceX, distanceY);}
  // </public>

  // <private>
  /**
    Begins a subpath [PDF:1.6:4.4.1].

    @param startPoint Starting point.
  */
  private void beginSubpath(
    Point2D startPoint
    )
  {
    add(
      new BeginSubpath(
        startPoint.getX(),
        scanner.getContentContext().getBox().getHeight() - startPoint.getY()
        )
      );
  }

  /**
    Begins a text object [PDF:1.6:5.3].

    @see #end()
  */
  private Text beginText(
    )
  {return (Text)begin(new Text());}

  //TODO: drawArc MUST seamlessly manage already-begun paths.
  private void drawArc(
    RectangularShape location,
    float startAngle,
    float endAngle,
    float branchWidth,
    float branchRatio,
    boolean beginPath
    )
  {
    /*
      NOTE: Strictly speaking, arc drawing is NOT a PDF primitive;
      it leverages the cubic bezier curve operator (thanks to
      G. Adam Stanislav, whose article was greatly inspirational:
      see http://www.whizkidtech.redprince.net/bezier/circle/).
    */

    if(startAngle > endAngle)
    {
      float swap = startAngle;
      startAngle = endAngle;
      endAngle = swap;
    }

    float radiusX = (float)location.getWidth() / 2;
    float radiusY = (float)location.getHeight() / 2;

    final Point2D center = new Point2D.Double(
      location.getX() + radiusX,
      location.getY() + radiusY
      );

    double radians1 = Math.toRadians(startAngle);
    Point2D point1 = new Point2D.Double(
      center.getX() + Math.cos(radians1) * radiusX,
      center.getY() - Math.sin(radians1) * radiusY
      );

    if(beginPath)
    {beginSubpath(point1);}

    final double endRadians = Math.toRadians(endAngle);
    final double quadrantRadians = Math.PI / 2;
    double radians2 = Math.min(
      radians1 + quadrantRadians - radians1 % quadrantRadians,
      endRadians
      );
    final double kappa = 0.5522847498;
    while(true)
    {
      double segmentX = radiusX * kappa;
      double segmentY = radiusY * kappa;

      // Endpoint 2.
      Point2D point2 = new Point2D.Double(
        center.getX() + Math.cos(radians2) * radiusX,
        center.getY() - Math.sin(radians2) * radiusY
        );

      // Control point 1.
      double tangentialRadians1 = Math.atan(
        -(Math.pow(radiusY,2) * (point1.getX()-center.getX()))
          / (Math.pow(radiusX,2) * (point1.getY()-center.getY()))
        );
      double segment1 = (
        segmentY * (1 - Math.abs(Math.sin(radians1)))
          + segmentX * (1 - Math.abs(Math.cos(radians1)))
        ) * (radians2-radians1) / quadrantRadians; // TODO: control segment calculation is still not so accurate as it should -- verify how to improve it!!!
      Point2D control1 = new Point2D.Double(
        point1.getX() + Math.abs(Math.cos(tangentialRadians1) * segment1) * Math.signum(-Math.sin(radians1)),
        point1.getY() + Math.abs(Math.sin(tangentialRadians1) * segment1) * Math.signum(-Math.cos(radians1))
        );

      // Control point 2.
      double tangentialRadians2 = Math.atan(
        -(Math.pow(radiusY,2) * (point2.getX()-center.getX()))
          / (Math.pow(radiusX,2) * (point2.getY()-center.getY()))
        );
      double segment2 = (
        segmentY * (1 - Math.abs(Math.sin(radians2)))
          + segmentX * (1 - Math.abs(Math.cos(radians2)))
        ) * (radians2-radians1) / quadrantRadians; // TODO: control segment calculation is still not so accurate as it should -- verify how to improve it!!!
      Point2D control2 = new Point2D.Double(
        point2.getX() + Math.abs(Math.cos(tangentialRadians2) * segment2) * Math.signum(Math.sin(radians2)),
        point2.getY() + Math.abs(Math.sin(tangentialRadians2) * segment2) * Math.signum(Math.cos(radians2))
        );

      // Draw the current quadrant curve!
      drawCurve(
        point2,
        control1,
        control2
        );

      // Last arc quadrant?
      if(radians2 == endRadians)
        break;

      // Preparing the next quadrant iteration...
      point1 = point2;
      radians1 = radians2;
      radians2 += quadrantRadians;
      if(radians2 > endRadians)
      {radians2 = endRadians;}

      double quadrantRatio = (radians2 - radians1) / quadrantRadians;
      radiusX += branchWidth * quadrantRatio;
      radiusY += branchWidth * quadrantRatio;

      branchWidth *= branchRatio;
    }
  }

  private PdfName getFontName(
    Font value
    )
  {
    // Ensuring that the font exists within the context resources...
    Resources resources = scanner.getContentContext().getResources();
    FontResources fonts = resources.getFonts();
    // No font resources collection?
    if(fonts == null)
    {
      // Create the font resources collection!
      fonts = new FontResources(scanner.getContents().getDocument());
      resources.setFonts(fonts); resources.update();
    }
    // Get the key associated to the font!
    PdfName name = fonts.getBaseDataObject().getKey(value.getBaseObject());
    // No key found?
    if(name == null)
    {
      // Insert the font within the resources!
      int fontIndex = fonts.size();
      do
      {name = new PdfName(String.valueOf(++fontIndex));}
      while(fonts.containsKey(name));
      fonts.put(name,value); fonts.update();
    }

    return name;
  }

  private PdfName getXObjectName(
    XObject value
    )
  {
    // Ensuring that the external object exists within the context resources...
    Resources resources = scanner.getContentContext().getResources();
    XObjectResources xObjects = resources.getXObjects();
    // No external object resources collection?
    if(xObjects == null)
    {
      // Create the external object resources collection!
      xObjects = new XObjectResources(scanner.getContents().getDocument());
      resources.setXObjects(xObjects); resources.update();
    }
    // Get the key associated to the external object!
    PdfName name = xObjects.getBaseDataObject().getKey(value.getBaseObject());
    // No key found?
    if(name == null)
    {
      // Insert the external object within the resources!
      int xObjectIndex = xObjects.size();
      do
      {name = new PdfName(String.valueOf(++xObjectIndex));}
      while(xObjects.containsKey(name));
      xObjects.put(name,value); xObjects.update();
    }

    return name;
  }

  private PdfName getColorSpaceName(
    ColorSpace<?> value
    )
  {
    if(value instanceof DeviceGrayColorSpace)
    {return PdfName.DeviceGray;}
    else if(value instanceof DeviceRGBColorSpace)
    {return PdfName.DeviceRGB;}
    else if(value instanceof DeviceCMYKColorSpace)
    {return PdfName.DeviceCMYK;}
    else
      throw new NotImplementedException("colorSpace MUST be converted to its associated name; you need to implement a method in PdfDictionary that, given a PdfDirectObject, returns its associated key.");
  }

  /**
    Applies a rotation to the coordinate system from text space to user space [PDF:1.6:4.2.2].

    @param angle Rotational counterclockwise angle.
  */
  @SuppressWarnings("unused")
  private void rotateText(
    float angle
    )
  {
    double rad = angle * Math.PI / 180;
    double cos = Math.cos(rad);
    double sin = Math.sin(rad);

    setTextMatrix(cos, sin, -sin, cos, 0, 0);
  }

  /**
    Applies a scaling to the coordinate system from text space to user space
    [PDF:1.6:4.2.2].
    @param ratioX Horizontal scaling ratio.
    @param ratioY Vertical scaling ratio.
  */
  @SuppressWarnings("unused")
  private void scaleText(
    float ratioX,
    float ratioY
    )
  {setTextMatrix(ratioX, 0, 0, ratioY, 0, 0);}

  /**
    Sets the transformation of the coordinate system from text space to user space [PDF:1.6:5.3.1].
    <h3>Remarks</h3>
    <p>The transformation replaces the current text matrix.</p>

    @param a Item 0,0 of the matrix.
    @param b Item 0,1 of the matrix.
    @param c Item 1,0 of the matrix.
    @param d Item 1,1 of the matrix.
    @param e Item 2,0 of the matrix.
    @param f Item 2,1 of the matrix.
  */
  private void setTextMatrix(
    double a,
    double b,
    double c,
    double d,
    double e,
    double f
    )
  {add(new SetTextMatrix(a,b,c,d,e,f));}

  /**
    Applies a translation to the coordinate system from text space
    to user space [PDF:1.6:4.2.2].

    @param distanceX Horizontal distance.
    @param distanceY Vertical distance.
  */
  private void translateText(
    float distanceX,
    float distanceY
    )
  {setTextMatrix(1, 0, 0, 1, distanceX, distanceY);}

  /**
    Applies a translation to the coordinate system from text space to user space,
    relative to the start of the current line [PDF:1.6:5.3.1].

    @param offsetX Horizontal offset.
    @param offsetY Vertical offset.
  */
  @SuppressWarnings("unused")
  private void translateTextRelative(
    float offsetX,
    float offsetY
    )
  {
    add(
      new TranslateTextRelative(
        offsetX,
        -offsetY
        )
      );
  }

  /**
    Applies a translation to the coordinate system from text space to user space,
    moving to the start of the next line [PDF:1.6:5.3.1].
  */
  @SuppressWarnings("unused")
  private void translateTextToNextLine(
    )
  {add(TranslateTextToNextLine.Value);}
  // </private>
  // </interface>
  // </dynamic>
  // </class>
}
TOP

Related Classes of org.pdfclown.documents.contents.composition.PrimitiveComposer

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.