blob: 446e75fe05b19b8e3baf9c33cf367f43a7ef7671 [file] [log] [blame]
<?php
/**
* File containing the ezcDocumentPdfPage class
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Document
* @version //autogen//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
* @access private
*/
/**
* PDF page class
*
* Class containing context information about a single rendered page.
*
* It especially encodes information about already covered / blocked areas on
* one PDF page, and offers methods to check if a new content block fits on the
* page an, where it does fit on the page.
*
* The testing for new boxes, where they fit on the page and in which
* dimensions they fit, is implemented in the testFitRectangle() method. The
* method implementation is optimized for speed, since it is called *a lot*
* during the rendering process.
*
* @package Document
* @access private
* @version //autogen//
*/
class ezcDocumentPdfPage implements ezcDocumentLocateable
{
/**
* Already covered areas, given as an arrays of ezcDocumentPdfBoundingBox
* objects.
*
* @var array
*/
protected $covered = array();
/**
* Current transaction
*
* @var mixed
*/
protected $transaction = 0;
/**
* Stored drawing positions for each transaction.
*
* @var array
*/
protected $storedPositions = array();
/**
* Page number
*
* @var int
*/
protected $pageNumber;
/**
* Current horizontal rendering position on page
*
* @var float
*/
public $x;
/**
* Current vertical rendering position on page
*
* @var float
*/
public $y;
/**
* Horizontal offset in a column
*
* @var float
*/
public $xOffset;
/**
* Horizontal width reduction in a column
*
* @var float
*/
public $xReduce;
/**
* X coordinate of rendering start position
*
* @var float
*/
public $startX;
/**
* Y coordinate of rendering start position
*
* @var float
*/
public $startY;
/**
* Width of current page - given in millimeters
*
* @var float
*/
protected $width;
/**
* Height of current page - given in millimeters
*
* @var float
*/
protected $height;
/**
* Inner width of current page - given in millimeters
*
* @var float
*/
protected $innerWidth;
/**
* Inner height of current page - given in millimeters
*
* @var float
*/
protected $innerHeight;
/**
* ID of the page.
*
* This ID defines an order on the pages. It is *not* sequential, there
* might always be holes in the sequence.
*
* But a page creted later in the rendering process will always have a
* higher number then the pages before.
*
* @var int
*/
protected $orderedId;
/**
* Global static ID provider to dertermine page creation order. This is
* required for the $orderedId property.
*
* @var int
*/
static protected $idCounter = 1;
/**
* Array of pages sizes
*
* Associates known page size identifiers the actual size in millimeters.
*
* @var array
*/
protected static $pageSizes = array(
'A0' => array( 841, 1189 ),
'A1' => array( 594, 841 ),
'A2' => array( 420, 594 ),
'A3' => array( 297, 420 ),
'A4' => array( 210, 297 ),
'A5' => array( 148, 210 ),
'A6' => array( 105, 148 ),
'A7' => array( 74, 105 ),
'A8' => array( 52, 74 ),
'A9' => array( 37, 52 ),
'A10' => array( 26, 37 ),
'B0' => array( 1000, 1414 ),
'B1' => array( 707, 1000 ),
'B2' => array( 500, 707 ),
'B3' => array( 353, 500 ),
'B4' => array( 250, 353 ),
'B5' => array( 176, 250 ),
'B6' => array( 125, 176 ),
'B7' => array( 88, 125 ),
'B8' => array( 62, 88 ),
'B9' => array( 44, 62 ),
'B10' => array( 31, 44 ),
'C0' => array( 917, 1297 ),
'C1' => array( 648, 917 ),
'C2' => array( 458, 648 ),
'C3' => array( 324, 458 ),
'C4' => array( 229, 324 ),
'C5' => array( 162, 229 ),
'C6' => array( 114, 162 ),
'C7' => array( 81, 114 ),
'C8' => array( 57, 81 ),
'C9' => array( 40, 57 ),
'C10' => array( 28, 40 ),
'RA0' => array( 860, 1220 ),
'RA1' => array( 610, 860 ),
'RA2' => array( 430, 610 ),
'RA3' => array( 305, 430 ),
'RA4' => array( 215, 305 ),
'SRA0' => array( 900, 1280 ),
'SRA1' => array( 640, 900 ),
'SRA2' => array( 450, 640 ),
'SRA3' => array( 320, 450 ),
'SRA4' => array( 225, 320 ),
'LETTER' => array( 215.9, 279.4 ),
'LEGAL' => array( 215.9, 355.6 ),
'EXECUTIVE' => array( 184.1, 266.7 ),
'FOLIO' => array( 215.9, 330.2 ),
'TEST' => array( 100, 100 ),
);
/**
* Construct new fresh page from its dimensions
*
* @param int $pageNumber
* @param float $width
* @param float $height
* @param mixed $innerWidth
* @param mixed $innerHeight
* @return void
*/
public function __construct( $pageNumber, $width, $height, $innerWidth = null, $innerHeight = null )
{
$this->pageNumber = (int) $pageNumber;
$this->width = (float) $width;
$this->height = (float) $height;
$this->innerWidth = $innerWidth === null ? $this->width : (float) $innerWidth;
$this->innerHeight = $innerHeight === null ? $this->height : (float) $innerHeight;
$this->orderedId = ++self::$idCounter;
}
/**
* Create from user readable soze specification
*
* Create page from common page size abbreviations, like "A4" and page
* orientation.
*
* @param int $pageNumber
* @param mixed $size
* @param mixed $orientation
* @param array $margin
* @param array $padding
* @return ezcDocumentPdfPage
*/
public static function createFromSpecification( $pageNumber, $size, $orientation, array $margin, array $padding )
{
if ( !isset( self::$pageSizes[$size] ) )
{
throw new ezcBaseValueException( "page-size", $size, implode( ', ', self::$pageSizes ) );
}
// Calculate border sizes, depending on assigned margin and
// padding
$topBorder = $margin['top'] + $padding['top'];
$leftBorder = $margin['left'] + $padding['left'];
$bottomBorder = $margin['bottom'] + $padding['bottom'];
$rightBorder = $margin['right'] + $padding['right'];
switch ( $orientation )
{
case 'landscape':
$page = new ezcDocumentPdfPage(
$pageNumber,
$width = self::$pageSizes[$size][1] + $margin['left'] + $margin['right'],
$height = self::$pageSizes[$size][0] + $margin['top'] + $margin['bottom'],
$width - $leftBorder - $rightBorder,
$height - $topBorder - $bottomBorder
);
break;
case 'portrait':
$page = new ezcDocumentPdfPage(
$pageNumber,
$width = self::$pageSizes[$size][0] + $margin['left'] + $margin['right'],
$height = self::$pageSizes[$size][1] + $margin['top'] + $margin['bottom'],
$width - $leftBorder - $rightBorder,
$height - $topBorder - $bottomBorder
);
break;
default:
throw new ezcBaseValueException( "page-orientation", $orientation, 'landscape or portrait' );
}
// Set cover boxes for areas covered by padding and margin
$page->setCovered( new ezcDocumentPdfBoundingBox( 0, 0, $width, $topBorder ) );
$page->setCovered( new ezcDocumentPdfBoundingBox( 0, 0, $leftBorder, $height ) );
$page->setCovered( new ezcDocumentPdfBoundingBox( 0, $height - $bottomBorder, $width, $bottomBorder ) );
$page->setCovered( new ezcDocumentPdfBoundingBox( $width - $rightBorder, 0, $rightBorder, $height ) );
// Update rendering start position
$page->x = $page->startX = $leftBorder;
$page->y = $page->startY = $topBorder;
return $page;
}
/**
* Wrapper for virtual property access
*
* @param string $property
* @return mixed
*/
public function __get( $property )
{
switch ( $property )
{
case 'number':
return $this->pageNumber;
case 'startX':
return $this->startX;
case 'startY':
return $this->startY;
case 'width':
return $this->width;
case 'height':
return $this->height;
case 'innerWidth':
return $this->innerWidth;
case 'innerHeight':
return $this->innerHeight;
case 'orderedId':
return $this->orderedId;
}
}
/**
* Start a new transaction sequence
*
* Start a new transaction, which will group all covered areas, until the
* next transaction is started. This methods takes and returns an
* identifier for this transaction, which can be used to commit this
* transaction, or revert everything since (including) this this
* transaction.
*
* @param mixed $transaction
* @return mixed
*/
public function startTransaction( $transaction )
{
$this->covered[$this->transaction = $transaction] = array();
$this->storedPositions[$this->transaction] = array( $this->x, $this->y );
return $this->transaction;
}
/**
* Revert transaction
*
* Revert all transactions after the specified (including the specified)
* transaction.
*
* @param mixed $transaction
* @return void
*/
public function revert( $transaction )
{
if ( !isset( $this->covered[$transaction] ) )
{
return false;
}
$remove = false;
foreach ( $this->covered as $id => $areas )
{
if ( !$remove &&
( $id !== $transaction ) )
{
continue;
}
$remove = true;
unset( $this->covered[$id] );
}
list( $this->x, $this->y ) = $this->storedPositions[$transaction];
return true;
}
/**
* Set space covered
*
* Append a rectangle of already covered space. This space will then not be
* reused for any other objects on the page.
*
* There is no check for overlapping of covered areas in here, so that you
* can add bounding boxes wrapping multiple already existing rectangles.
*
* Returns an array specifying the transaction and ID of the cover action.
* This tupel may be used later to call the uncover() method, to remove
* this coverage area again.
*
* @param ezcDocumentPdfBoundingBox $rectangle
* @param mixed $id
* @return array
*/
public function setCovered( ezcDocumentPdfBoundingBox $rectangle, $id = null )
{
$this->covered[$this->transaction][] = $rectangle;
return array( $this->transaction, count( $this->covered[$this->transaction] ) - 1 );
}
/**
* Uncover area
*
* Uncover the area specified by the ID returned by the setCovered()
* method.
*
* Will return false, if the given ID is unknown in the transaction.
*
* @param array $id
* @return bool
*/
public function uncover( array $id )
{
if ( isset( $this->covered[$id[0]] ) &&
isset( $this->covered[$id[0]][$id[1]] ) )
{
unset( $this->covered[$id[0]][$id[1]] );
return true;
}
return false;
}
/**
* Try to fit specified rectangle on page
*
* Try to find place for the specified rectangle on the curernt page. Each
* of the parameters may be set to null, which means that this parameter
* can be varied in dimension or value.
*
* If all parameters are set to a fixed value, either false will be
* returned, if the location is already (partly) covered, or a rectangle
* will be returned if that space is still available.
*
* If, for example, the yPos parameter is set to null, but all other
* parameters are set, the box will be moved down the page, until a
* available location could be found.
*
* @param mixed $xPos
* @param mixed $yPos
* @param mixed $width
* @param mixed $height
* @return mixed
*/
public function testFitRectangle( $xPos = null, $yPos = null, $width = null, $height = null )
{
// Ensure requested area is within the page boundings
if ( ( $xPos < 0 ) ||
( $yPos < 0 ) ||
( ( $xPos + $this->xOffset + $width ) > $this->width ) ||
( ( $yPos + $height ) > $this->height ) )
{
return false;
}
// Store aspects of passed parameters
$moveX = ( $xPos === null );
$moveY = ( $yPos === null );
$adjustWidth = ( $width === null );
$adjustHeight = ( $height === null );
$boundings = new ezcDocumentPdfBoundingBox( $xPos, $yPos, $width, $height );
// We do not support moving and extending in the same direction yet,
// since this would require some sort of backtracking.
if ( ( $moveX && $adjustWidth ) ||
( $moveY && $adjustHeight ) )
{
throw new ezcBaseFunctionalityNotSupportedException(
'Moving and extensions ins same direction',
'Backtracking would be required'
);
}
// Start width adjusting with full page width, will be reduced later
// based on found boxes.
if ( $adjustWidth )
{
$boundings->width = $this->width - $boundings->x - $this->xOffset - $this->xReduce;
}
// Start height adjusting with full page height, will be reduced later
// based on found boxes.
if ( $adjustHeight )
{
$boundings->height = $this->height - $boundings->y;
}
// Test all covered areas for intersections with the given bounding box
foreach ( $this->covered as $transaction => $areas )
{
foreach ( $areas as $covered )
{
// These variables indicate which bounding box checks evaluated to
// true, so we can handle bounding box modififactions according to
// this.
$xOut = 0;
$yOut = 0;
// Do NOT change the test order.
if ( ( // Test for left coordinate in covering boundings
( $xOut |= ( ( $boundings->x > $covered->x ) &&
( $boundings->x < ( $covered->x + $covered->width ) ) ) << 1 ) ||
// Test for right coordinate in covering boundings
( $xOut |= ( ( ( $boundings->x + $boundings->width ) > $covered->x ) &&
( ( $boundings->x + $boundings->width ) < ( $covered->x + $covered->width ) ) ) << 2 ) ||
// Test if coordinates outer wrap coverings
( $xOut |= ( ( $boundings->x <= $covered->x ) &&
( ( $boundings->x + $boundings->width ) >= ( $covered->x + $covered->width ) ) ) << 3 )
) &&
( // Test for top coordinate in covering boundings
( $yOut |= ( ( $boundings->y > $covered->y ) &&
( $boundings->y < ( $covered->y + $covered->height ) ) ) << 1 ) ||
// Test for bottom coordinate in covering boundings
( $yOut |= ( ( ( $boundings->y + $boundings->height ) > $covered->y ) &&
( ( $boundings->y + $boundings->height ) < ( $covered->y + $covered->height ) ) ) << 2 ) ||
// Test if coordinates outer wrap coverings
( $yOut |= ( ( $boundings->y <= $covered->y ) &&
( ( $boundings->y + $boundings->height ) >= ( $covered->y + $covered->height ) ) ) << 3 )
) )
{
// Adjust bounding box width, if only the right coordinate hit
// the covered area.
if ( $adjustWidth &&
( $xOut & 12 ) )
{
$boundings->width = $covered->x - $boundings->x;
}
// Adjust bounding box width, if only the right coordinate hit
// the covered area.
if ( $adjustHeight &&
( $yOut & 12 ) )
{
$boundings->height = $covered->y - $boundings->y;
}
// If the width or height has been adjusted, we did not hit any
// covered area with the starting coordinates because of the
// test order in the if statement above. We can safely continue
// to check the next covering area. We cannot do the continue
// in one of the blocks above, because we might need to modify
// both.
if ( ( $adjustWidth &&
( $xOut & 12 ) ) ||
( $adjustHeight &&
( $yOut & 12 ) ) )
{
continue;
}
if ( !$moveX && !$moveY )
{
// We hit something and may not move or adjust the box -
// break.
return false;
}
elseif ( $moveX && $moveY )
{
// Move in the direction where less movement is required.
// This might be imporved by additionally checking already
// reached page boundings...
$xMovement = ( $covered->x + $covered->width ) - $boundings->x;
$yMovement = ( $covered->y + $covered->height ) - $boundings->y;
$boundings->x += $xMovement > $yMovement ? 0 : $xMovement;
$boundings->y += $yMovement > $xMovement ? 0 : $yMovement;
}
elseif ( $moveX )
{
$boundings->x = $covered->x + $covered->width;
}
elseif ( $moveY )
{
$boundings->y = $covered->y + $covered->height;
}
}
}
}
// Recheck moved bounding box, to check if it still fits page
// boundings, and has not been moved into any covered areas at the
// bottom right side of the page.
if ( $moveX || $moveY )
{
return $this->testFitRectangle( $boundings->x, $boundings->y, $boundings->width, $boundings->height );
}
$boundings->x += $this->xOffset;
return $boundings;
}
/**
* Get elements location ID
*
* Return the elements location ID, based on the factors described in the
* class header.
*
* @return string
*/
public function getLocationId()
{
return '/page' .
'.' . ( $this->pageNumber % 2 ? 'left' : 'right' ) .
'#page_' . $this->pageNumber;
}
}
?>