This sounded like a simple job. Print out address labels from a database in a certain format (8 x 2 labels on a page) in a way that can then be printed directly onto the label sheets. Easy right?

Not so much. HTML and CSS3 is supposed to add a load of print functionality and physical sizes but they don't work well at all. Browser treat them all differently, Chrome applies margins in addition to what you set in CSS so everything gets squashed and whatever I tried, it didn't seem to make sense. On top of that, the Developer tools allow you to render using the print css but this does not really allow a real print preview while tweaking the styles.

Fortunately, I chanced upon a suggestion to use FPDF, a PHP library to generate PDFs in code. It looked easy enough although unfortunately, you cannot simply create a fixed size "cell" and wrap text in it. A Cell is one line of text and multi-cell will simply create more cells for each new line of text. Not quite right but fortunately, using the position functions setX, setY etc. the maths is relatively simple to keep track of column number, row number and then work out where to add a new page.

Use the following code as a reference - note it is from Yii 2 framework and so some of this won't be relevant to you. Then check out the notes below for additional help.

public function actionAddresslabels()
{
$request = \Yii::$app->request;

// Set defaults for layout
$cols = $request->get('cols', 2); // Number of columns
$rows = $request->get('rows', 8); // Number of rows
$top = $request->get('top', 8); // Top margin in mm
$left = $request->get('left', 5); // Left margin in mm
$vspacing = $request->get('vspacing', 0); // Spacing vertically between each label in mm (excludes outside margins)
$hspacing = $request->get('hspacing', 2.5); // Spacing horizontally between columns in mm (excludes outside margins)
$padding = $request->get('padding', 3);

// Compute some numbers
$pageSize = $rows * $cols;
$colSpacing = (210.0 - (2*$left) + $hspacing) / ($cols);
$rowSpacing = (297.0 - (2*$top) + $vspacing) / ($rows);

$dataProvider = new ActiveDataProvider([
'query' => User::find()
->joinWith(['applications'])
->where(['year' => Date('Y')]),
'pagination' => false,
]);

// Load data into local variables for loop
$models = $dataProvider->getModels();
$modelCount = $dataProvider->getCount();
$currentModel = 0;
$currentY = 0;
$currentX = 0;

// Basic setup of PDF
$pdf = new FPDF();
$pdf->SetLeftMargin($left + $padding);
$pdf->SetTopMargin($top + $padding);
$pdf->SetFont('Arial','',11);
$pdf->SetAutoPageBreak(false);

// For each cols x rows of addresses, add a page and render them correctly
while ( $currentModel < $modelCount )
{
if ( $currentModel % $pageSize === 0)
{
$pdf->AddPage();
$currentX = $left + $padding;
$currentY = $top + $padding;
}
$pdf->SetXY($currentX,$currentY);
$pdf->SetLeftMargin($currentX);
$model = $models[$currentModel];
$this->writeAddressLabel($pdf, $model);
$currentY += $rowSpacing;
if ( $currentY > (297 - 20) )
{
$currentY = $top + $padding;
$currentX += $colSpacing;
}
$currentModel++;
}

$this->layout = false;
\Yii::$app->response->format = \yii\web\Response::FORMAT_RAW;
$pdf->Output();
\Yii::$app->end();
}


  • The first section allows you to pass different values from the defaults into the query string for this action.
  • $padding allows all the text to be in from the top-left corner of each label and needs to be included in various calculations
  • The second section does some calculations for page size (number of labels total per page), column and row spacing are the pitch values so include the gutters between the labels.
  • The ActiveDataProvider is simply how I am querying the people to produce labels for. What I end up with is an array of objects ($models) that I will pull the individual address parts from.
  • $modelCount is simply used to control how long the loop below will continue for
  • The next section sets some static values for the PDF instance. The margins will shift all of the setXY stuff in from the edges of the page.
  • The main loop goes through all of the "users" in my models array 1 by one. 
  • The first section inside the loop uses mod arithmetic to see whether the current item is the first on a page, in which case a new page is created, and the X and Y positions are reset (they are relative to the current page, not the entire document).
  • The cursor is then positioned with SetXY
  • SetLeftMargin is called to ensure the current column has a hard left edge, otherwise the text becomes indented.
  • The method WriteAddressLabel is a helper method in my class that simply contains a number of calls to $pdf->Write(5,$model->town.PHP_EOL); With some wrapped in if ( $model->address3 !== "") so that they are not printed if blank. In your code, they might equate to null but in my code, they are blank strings if not set.
  • After the address is written, the Y position is moved down by a label pitch and then if this goes below the bottom of the page (hard-coded for A4 paper size 297mm minus a margin), then the column is incremented, Y is reset back to the top. We do not need to check for the column overflowing the page, since the mod arithmetic at the top of the loop will automatically create a new page when we have written the total number of items on the page.
  • The 4 lines below the loop are partly methods to tell Yii to output the correct format and not render a HTML layout and the call to $pdf->Output() closes the document and sends it to the standard output, which in this case is the response object.