Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * @package Cpdf
4
 */
5
/**
6
* Cpdf
7
*
8
*
9
* A PHP class to provide the basic functionality to create a pdf document without
10
* any requirement for additional modules.
11
*
12
* Note that they companion class CezPdf can be used to extend this class and dramatically
13
* simplify the creation of documents.
14
*
15
* IMPORTANT NOTE
16
* there is no warranty, implied or otherwise with this software.
17
*
18
* LICENCE
19
* This code has been placed in the Public Domain for all to enjoy.
20
*
21
* @author		Wayne Munro <pdf@ros.co.nz>
22
* @version 	009
23
* @package Cpdf
24
* @link http://www.ros.co.nz/pdf
25
*/
26
class Cpdf {
27
 
28
/**
29
* the current number of pdf objects in the document
30
*/
31
var $numObj=0;
32
/**
33
* this array contains all of the pdf objects, ready for final assembly
34
*/
35
var $objects = array();
36
/**
37
* the objectId (number within the objects array) of the document catalog
38
*/
39
var $catalogId;
40
/**
41
* array carrying information about the fonts that the system currently knows about
42
* used to ensure that a font is not loaded twice, among other things
43
*/
44
var $fonts=array();
45
/**
46
* a record of the current font
47
*/
48
var $currentFont='';
49
/**
50
* the current base font
51
*/
52
var $currentBaseFont='';
53
/**
54
* the number of the current font within the font array
55
*/
56
var $currentFontNum=0;
57
/**
58
*
59
*/
60
var $currentNode;
61
/**
62
* object number of the current page
63
*/
64
var $currentPage;
65
/**
66
* object number of the currently active contents block
67
*/
68
var $currentContents;
69
/**
70
* number of fonts within the system
71
*/
72
var $numFonts=0;
73
/**
74
* current colour for fill operations, defaults to inactive value, all three components should be between 0 and 1 inclusive when active
75
*/
76
var $currentColour=array('r'=>-1,'g'=>-1,'b'=>-1);
77
/**
78
* current colour for stroke operations (lines etc.)
79
*/
80
var $currentStrokeColour=array('r'=>-1,'g'=>-1,'b'=>-1);
81
/**
82
* current style that lines are drawn in
83
*/
84
var $currentLineStyle='';
85
/**
86
* an array which is used to save the state of the document, mainly the colours and styles
87
* it is used to temporarily change to another state, the change back to what it was before
88
*/
89
var $stateStack = array();
90
/**
91
* number of elements within the state stack
92
*/
93
var $nStateStack = 0;
94
/**
95
* number of page objects within the document
96
*/
97
var $numPages=0;
98
/**
99
* object Id storage stack
100
*/
101
var $stack=array();
102
/**
103
* number of elements within the object Id storage stack
104
*/
105
var $nStack=0;
106
/**
107
* an array which contains information about the objects which are not firmly attached to pages
108
* these have been added with the addObject function
109
*/
110
var $looseObjects=array();
111
/**
112
* array contains infomation about how the loose objects are to be added to the document
113
*/
114
var $addLooseObjects=array();
115
/**
116
* the objectId of the information object for the document
117
* this contains authorship, title etc.
118
*/
119
var $infoObject=0;
120
/**
121
* number of images being tracked within the document
122
*/
123
var $numImages=0;
124
/**
125
* an array containing options about the document
126
* it defaults to turning on the compression of the objects
127
*/
128
var $options=array('compression'=>1);
129
/**
130
* the objectId of the first page of the document
131
*/
132
var $firstPageId;
133
/**
134
* used to track the last used value of the inter-word spacing, this is so that it is known
135
* when the spacing is changed.
136
*/
137
var $wordSpaceAdjust=0;
138
/**
139
* the object Id of the procset object
140
*/
141
var $procsetObjectId;
142
/**
143
* store the information about the relationship between font families
144
* this used so that the code knows which font is the bold version of another font, etc.
145
* the value of this array is initialised in the constuctor function.
146
*/
147
var $fontFamilies = array();
148
/**
149
* track if the current font is bolded or italicised
150
*/
151
var $currentTextState = '';
152
/**
153
* messages are stored here during processing, these can be selected afterwards to give some useful debug information
154
*/
155
var $messages='';
156
/**
157
* the ancryption array for the document encryption is stored here
158
*/
159
var $arc4='';
160
/**
161
* the object Id of the encryption information
162
*/
163
var $arc4_objnum=0;
164
/**
165
* the file identifier, used to uniquely identify a pdf document
166
*/
167
var $fileIdentifier='';
168
/**
169
* a flag to say if a document is to be encrypted or not
170
*/
171
var $encrypted=0;
172
/**
173
* the ancryption key for the encryption of all the document content (structure is not encrypted)
174
*/
175
var $encryptionKey='';
176
/**
177
* array which forms a stack to keep track of nested callback functions
178
*/
179
var $callback = array();
180
/**
181
* the number of callback functions in the callback array
182
*/
183
var $nCallback = 0;
184
/**
185
* store label->id pairs for named destinations, these will be used to replace internal links
186
* done this way so that destinations can be defined after the location that links to them
187
*/
188
var $destinations = array();
189
/**
190
* store the stack for the transaction commands, each item in here is a record of the values of all the
191
* variables within the class, so that the user can rollback at will (from each 'start' command)
192
* note that this includes the objects array, so these can be large.
193
*/
194
var $checkpoint = '';
195
/**
196
* class constructor
197
* this will start a new document
198
* @var array array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero.
199
*/
200
function Cpdf ($pageSize=array(0,0,612,792)){
201
  $this->newDocument($pageSize);
202
 
203
  // also initialize the font families that are known about already
204
  $this->setFontFamily('init');
205
//  $this->fileIdentifier = md5('xxxxxxxx'.time());
206
 
207
}
208
 
209
/**
210
* Document object methods (internal use only)
211
*
212
* There is about one object method for each type of object in the pdf document
213
* Each function has the same call list ($id,$action,$options).
214
* $id = the object ID of the object, or what it is to be if it is being created
215
* $action = a string specifying the action to be performed, though ALL must support:
216
*           'new' - create the object with the id $id
217
*           'out' - produce the output for the pdf object
218
* $options = optional, a string or array containing the various parameters for the object
219
*
220
* These, in conjunction with the output function are the ONLY way for output to be produced
221
* within the pdf 'file'.
222
*/
223
 
224
/**
225
*destination object, used to specify the location for the user to jump to, presently on opening
226
*/
227
function o_destination($id,$action,$options=''){
228
  if ($action!='new'){
229
    $o =& $this->objects[$id];
230
  }
231
  switch($action){
232
    case 'new':
233
      $this->objects[$id]=array('t'=>'destination','info'=>array());
234
      $tmp = '';
235
      switch ($options['type']){
236
        case 'XYZ':
237
        case 'FitR':
238
          $tmp =  ' '.$options['p3'].$tmp;
239
        case 'FitH':
240
        case 'FitV':
241
        case 'FitBH':
242
        case 'FitBV':
243
          $tmp =  ' '.$options['p1'].' '.$options['p2'].$tmp;
244
        case 'Fit':
245
        case 'FitB':
246
          $tmp =  $options['type'].$tmp;
247
          $this->objects[$id]['info']['string']=$tmp;
248
          $this->objects[$id]['info']['page']=$options['page'];
249
      }
250
      break;
251
    case 'out':
252
      $tmp = $o['info'];
253
      $res="\n".$id." 0 obj\n".'['.$tmp['page'].' 0 R /'.$tmp['string']."]\nendobj\n";
254
      return $res;
255
      break;
256
  }
257
}
258
 
259
/**
260
* set the viewer preferences
261
*/
262
function o_viewerPreferences($id,$action,$options=''){
263
  if ($action!='new'){
264
    $o =& $this->objects[$id];
265
  }
266
  switch ($action){
267
    case 'new':
268
      $this->objects[$id]=array('t'=>'viewerPreferences','info'=>array());
269
      break;
270
    case 'add':
271
      foreach($options as $k=>$v){
272
        switch ($k){
273
          case 'HideToolbar':
274
          case 'HideMenubar':
275
          case 'HideWindowUI':
276
          case 'FitWindow':
277
          case 'CenterWindow':
278
          case 'NonFullScreenPageMode':
279
          case 'Direction':
280
            $o['info'][$k]=$v;
281
          break;
282
        }
283
      }
284
      break;
285
    case 'out':
286
 
287
      $res="\n".$id." 0 obj\n".'<< ';
288
      foreach($o['info'] as $k=>$v){
289
        $res.="\n/".$k.' '.$v;
290
      }
291
      $res.="\n>>\n";
292
      return $res;
293
      break;
294
  }
295
}
296
 
297
/**
298
* define the document catalog, the overall controller for the document
299
*/
300
function o_catalog($id,$action,$options=''){
301
  if ($action!='new'){
302
    $o =& $this->objects[$id];
303
  }
304
  switch ($action){
305
    case 'new':
306
      $this->objects[$id]=array('t'=>'catalog','info'=>array());
307
      $this->catalogId=$id;
308
      break;
309
    case 'outlines':
310
    case 'pages':
311
    case 'openHere':
312
      $o['info'][$action]=$options;
313
      break;
314
    case 'viewerPreferences':
315
      if (!isset($o['info']['viewerPreferences'])){
316
        $this->numObj++;
317
        $this->o_viewerPreferences($this->numObj,'new');
318
        $o['info']['viewerPreferences']=$this->numObj;
319
      }
320
      $vp = $o['info']['viewerPreferences'];
321
      $this->o_viewerPreferences($vp,'add',$options);
322
      break;
323
    case 'out':
324
      $res="\n".$id." 0 obj\n".'<< /Type /Catalog';
325
      foreach($o['info'] as $k=>$v){
326
        switch($k){
327
          case 'outlines':
328
            $res.="\n".'/Outlines '.$v.' 0 R';
329
            break;
330
          case 'pages':
331
            $res.="\n".'/Pages '.$v.' 0 R';
332
            break;
333
          case 'viewerPreferences':
334
            $res.="\n".'/ViewerPreferences '.$o['info']['viewerPreferences'].' 0 R';
335
            break;
336
          case 'openHere':
337
            $res.="\n".'/OpenAction '.$o['info']['openHere'].' 0 R';
338
            break;
339
        }
340
      }
341
      $res.=" >>\nendobj";
342
      return $res;
343
      break;
344
  }
345
}
346
 
347
/**
348
* object which is a parent to the pages in the document
349
*/
350
function o_pages($id,$action,$options=''){
351
  if ($action!='new'){
352
    $o =& $this->objects[$id];
353
  }
354
  switch ($action){
355
    case 'new':
356
      $this->objects[$id]=array('t'=>'pages','info'=>array());
357
      $this->o_catalog($this->catalogId,'pages',$id);
358
      break;
359
    case 'page':
360
      if (!is_array($options)){
361
        // then it will just be the id of the new page
362
        $o['info']['pages'][]=$options;
363
      } else {
364
        // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative
365
        // and pos is either 'before' or 'after', saying where this page will fit.
366
        if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])){
367
          $i = array_search($options['rid'],$o['info']['pages']);
368
          if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i]==$options['rid']){
369
            // then there is a match
370
            // make a space
371
            switch ($options['pos']){
372
              case 'before':
373
                $k = $i;
374
                break;
375
              case 'after':
376
                $k=$i+1;
377
                break;
378
              default:
379
                $k=-1;
380
                break;
381
            }
382
            if ($k>=0){
383
              for ($j=count($o['info']['pages'])-1;$j>=$k;$j--){
384
                $o['info']['pages'][$j+1]=$o['info']['pages'][$j];
385
              }
386
              $o['info']['pages'][$k]=$options['id'];
387
            }
388
          }
389
        }
390
      }
391
      break;
392
    case 'procset':
393
      $o['info']['procset']=$options;
394
      break;
395
    case 'mediaBox':
396
      $o['info']['mediaBox']=$options; // which should be an array of 4 numbers
397
      break;
398
    case 'font':
399
      $o['info']['fonts'][]=array('objNum'=>$options['objNum'],'fontNum'=>$options['fontNum']);
400
      break;
401
    case 'xObject':
402
      $o['info']['xObjects'][]=array('objNum'=>$options['objNum'],'label'=>$options['label']);
403
      break;
404
    case 'out':
405
      if (count($o['info']['pages'])){
406
        $res="\n".$id." 0 obj\n<< /Type /Pages\n/Kids [";
407
        foreach($o['info']['pages'] as $k=>$v){
408
          $res.=$v." 0 R\n";
409
        }
410
        $res.="]\n/Count ".count($this->objects[$id]['info']['pages']);
411
        if ((isset($o['info']['fonts']) && count($o['info']['fonts'])) || isset($o['info']['procset'])){
412
          $res.="\n/Resources <<";
413
          if (isset($o['info']['procset'])){
414
            $res.="\n/ProcSet ".$o['info']['procset']." 0 R";
415
          }
416
          if (isset($o['info']['fonts']) && count($o['info']['fonts'])){
417
            $res.="\n/Font << ";
418
            foreach($o['info']['fonts'] as $finfo){
419
              $res.="\n/F".$finfo['fontNum']." ".$finfo['objNum']." 0 R";
420
            }
421
            $res.=" >>";
422
          }
423
          if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])){
424
            $res.="\n/XObject << ";
425
            foreach($o['info']['xObjects'] as $finfo){
426
              $res.="\n/".$finfo['label']." ".$finfo['objNum']." 0 R";
427
            }
428
            $res.=" >>";
429
          }
430
          $res.="\n>>";
431
          if (isset($o['info']['mediaBox'])){
432
            $tmp=$o['info']['mediaBox'];
433
            $res.="\n/MediaBox [".sprintf('%.3f',$tmp[0]).' '.sprintf('%.3f',$tmp[1]).' '.sprintf('%.3f',$tmp[2]).' '.sprintf('%.3f',$tmp[3]).']';
434
          }
435
        }
436
        $res.="\n >>\nendobj";
437
      } else {
438
        $res="\n".$id." 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj";
439
      }
440
      return $res;
441
    break;
442
  }
443
}
444
 
445
/**
446
* define the outlines in the doc, empty for now
447
*/
448
function o_outlines($id,$action,$options=''){
449
  if ($action!='new'){
450
    $o =& $this->objects[$id];
451
  }
452
  switch ($action){
453
    case 'new':
454
      $this->objects[$id]=array('t'=>'outlines','info'=>array('outlines'=>array()));
455
      $this->o_catalog($this->catalogId,'outlines',$id);
456
      break;
457
    case 'outline':
458
      $o['info']['outlines'][]=$options;
459
      break;
460
    case 'out':
461
      if (count($o['info']['outlines'])){
462
        $res="\n".$id." 0 obj\n<< /Type /Outlines /Kids [";
463
        foreach($o['info']['outlines'] as $k=>$v){
464
          $res.=$v." 0 R ";
465
        }
466
        $res.="] /Count ".count($o['info']['outlines'])." >>\nendobj";
467
      } else {
468
        $res="\n".$id." 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj";
469
      }
470
      return $res;
471
      break;
472
  }
473
}
474
 
475
/**
476
* an object to hold the font description
477
*/
478
function o_font($id,$action,$options=''){
479
  if ($action!='new'){
480
    $o =& $this->objects[$id];
481
  }
482
  switch ($action){
483
    case 'new':
484
      $this->objects[$id]=array('t'=>'font','info'=>array('name'=>$options['name'],'SubType'=>'Type1'));
485
      $fontNum=$this->numFonts;
486
      $this->objects[$id]['info']['fontNum']=$fontNum;
487
      // deal with the encoding and the differences
488
      if (isset($options['differences'])){
489
        // then we'll need an encoding dictionary
490
        $this->numObj++;
491
        $this->o_fontEncoding($this->numObj,'new',$options);
492
        $this->objects[$id]['info']['encodingDictionary']=$this->numObj;
493
      } else if (isset($options['encoding'])){
494
        // we can specify encoding here
495
        switch($options['encoding']){
496
          case 'WinAnsiEncoding':
497
          case 'MacRomanEncoding':
498
          case 'MacExpertEncoding':
499
            $this->objects[$id]['info']['encoding']=$options['encoding'];
500
            break;
501
          case 'none':
502
            break;
503
          default:
504
            $this->objects[$id]['info']['encoding']='WinAnsiEncoding';
505
            break;
506
        }
507
      } else {
508
        $this->objects[$id]['info']['encoding']='WinAnsiEncoding';
509
      }
510
      // also tell the pages node about the new font
511
      $this->o_pages($this->currentNode,'font',array('fontNum'=>$fontNum,'objNum'=>$id));
512
      break;
513
    case 'add':
514
      foreach ($options as $k=>$v){
515
        switch ($k){
516
          case 'BaseFont':
517
            $o['info']['name'] = $v;
518
            break;
519
          case 'FirstChar':
520
          case 'LastChar':
521
          case 'Widths':
522
          case 'FontDescriptor':
523
          case 'SubType':
524
          $this->addMessage('o_font '.$k." : ".$v);
525
            $o['info'][$k] = $v;
526
            break;
527
        }
528
     }
529
      break;
530
    case 'out':
531
      $res="\n".$id." 0 obj\n<< /Type /Font\n/Subtype /".$o['info']['SubType']."\n";
532
      $res.="/Name /F".$o['info']['fontNum']."\n";
533
      $res.="/BaseFont /".$o['info']['name']."\n";
534
      if (isset($o['info']['encodingDictionary'])){
535
        // then place a reference to the dictionary
536
        $res.="/Encoding ".$o['info']['encodingDictionary']." 0 R\n";
537
      } else if (isset($o['info']['encoding'])){
538
        // use the specified encoding
539
        $res.="/Encoding /".$o['info']['encoding']."\n";
540
      }
541
      if (isset($o['info']['FirstChar'])){
542
        $res.="/FirstChar ".$o['info']['FirstChar']."\n";
543
      }
544
      if (isset($o['info']['LastChar'])){
545
        $res.="/LastChar ".$o['info']['LastChar']."\n";
546
      }
547
      if (isset($o['info']['Widths'])){
548
        $res.="/Widths ".$o['info']['Widths']." 0 R\n";
549
      }
550
      if (isset($o['info']['FontDescriptor'])){
551
        $res.="/FontDescriptor ".$o['info']['FontDescriptor']." 0 R\n";
552
      }
553
      $res.=">>\nendobj";
554
      return $res;
555
      break;
556
  }
557
}
558
 
559
/**
560
* a font descriptor, needed for including additional fonts
561
*/
562
function o_fontDescriptor($id,$action,$options=''){
563
  if ($action!='new'){
564
    $o =& $this->objects[$id];
565
  }
566
  switch ($action){
567
    case 'new':
568
      $this->objects[$id]=array('t'=>'fontDescriptor','info'=>$options);
569
      break;
570
    case 'out':
571
      $res="\n".$id." 0 obj\n<< /Type /FontDescriptor\n";
572
      foreach ($o['info'] as $label => $value){
573
        switch ($label){
574
          case 'Ascent':
575
          case 'CapHeight':
576
          case 'Descent':
577
          case 'Flags':
578
          case 'ItalicAngle':
579
          case 'StemV':
580
          case 'AvgWidth':
581
          case 'Leading':
582
          case 'MaxWidth':
583
          case 'MissingWidth':
584
          case 'StemH':
585
          case 'XHeight':
586
          case 'CharSet':
587
            if (strlen($value)){
588
              $res.='/'.$label.' '.$value."\n";
589
            }
590
            break;
591
          case 'FontFile':
592
          case 'FontFile2':
593
          case 'FontFile3':
594
            $res.='/'.$label.' '.$value." 0 R\n";
595
            break;
596
          case 'FontBBox':
597
            $res.='/'.$label.' ['.$value[0].' '.$value[1].' '.$value[2].' '.$value[3]."]\n";
598
            break;
599
          case 'FontName':
600
            $res.='/'.$label.' /'.$value."\n";
601
            break;
602
        }
603
      }
604
      $res.=">>\nendobj";
605
      return $res;
606
      break;
607
  }
608
}
609
 
610
/**
611
* the font encoding
612
*/
613
function o_fontEncoding($id,$action,$options=''){
614
  if ($action!='new'){
615
    $o =& $this->objects[$id];
616
  }
617
  switch ($action){
618
    case 'new':
619
      // the options array should contain 'differences' and maybe 'encoding'
620
      $this->objects[$id]=array('t'=>'fontEncoding','info'=>$options);
621
      break;
622
    case 'out':
623
      $res="\n".$id." 0 obj\n<< /Type /Encoding\n";
624
      if (!isset($o['info']['encoding'])){
625
        $o['info']['encoding']='WinAnsiEncoding';
626
      }
627
      if ($o['info']['encoding']!='none'){
628
        $res.="/BaseEncoding /".$o['info']['encoding']."\n";
629
      }
630
      $res.="/Differences \n[";
631
      $onum=-100;
632
      foreach($o['info']['differences'] as $num=>$label){
633
        if ($num!=$onum+1){
634
          // we cannot make use of consecutive numbering
635
          $res.= "\n".$num." /".$label;
636
        } else {
637
          $res.= " /".$label;
638
        }
639
        $onum=$num;
640
      }
641
      $res.="\n]\n>>\nendobj";
642
      return $res;
643
      break;
644
  }
645
}
646
 
647
/**
648
* the document procset, solves some problems with printing to old PS printers
649
*/
650
function o_procset($id,$action,$options=''){
651
  if ($action!='new'){
652
    $o =& $this->objects[$id];
653
  }
654
  switch ($action){
655
    case 'new':
656
      $this->objects[$id]=array('t'=>'procset','info'=>array('PDF'=>1,'Text'=>1));
657
      $this->o_pages($this->currentNode,'procset',$id);
658
      $this->procsetObjectId=$id;
659
      break;
660
    case 'add':
661
      // this is to add new items to the procset list, despite the fact that this is considered
662
      // obselete, the items are required for printing to some postscript printers
663
      switch ($options) {
664
        case 'ImageB':
665
        case 'ImageC':
666
        case 'ImageI':
667
          $o['info'][$options]=1;
668
          break;
669
      }
670
      break;
671
    case 'out':
672
      $res="\n".$id." 0 obj\n[";
673
      foreach ($o['info'] as $label=>$val){
674
        $res.='/'.$label.' ';
675
      }
676
      $res.="]\nendobj";
677
      return $res;
678
      break;
679
  }
680
}
681
 
682
/**
683
* define the document information
684
*/
685
function o_info($id,$action,$options=''){
686
  if ($action!='new'){
687
    $o =& $this->objects[$id];
688
  }
689
  switch ($action){
690
    case 'new':
691
      $this->infoObject=$id;
692
      $date='D:'.date('Ymd');
693
      $this->objects[$id]=array('t'=>'info','info'=>array('Creator'=>'R and OS php pdf writer, http://www.ros.co.nz','CreationDate'=>$date));
694
      break;
695
    case 'Title':
696
    case 'Author':
697
    case 'Subject':
698
    case 'Keywords':
699
    case 'Creator':
700
    case 'Producer':
701
    case 'CreationDate':
702
    case 'ModDate':
703
    case 'Trapped':
704
      $o['info'][$action]=$options;
705
      break;
706
    case 'out':
707
      if ($this->encrypted){
708
        $this->encryptInit($id);
709
      }
710
      $res="\n".$id." 0 obj\n<<\n";
711
      foreach ($o['info']  as $k=>$v){
712
        $res.='/'.$k.' (';
713
        if ($this->encrypted){
714
          $res.=$this->filterText($this->ARC4($v));
715
        } else {
716
          $res.=$this->filterText($v);
717
        }
718
        $res.=")\n";
719
      }
720
      $res.=">>\nendobj";
721
      return $res;
722
      break;
723
  }
724
}
725
 
726
/**
727
* an action object, used to link to URLS initially
728
*/
729
function o_action($id,$action,$options=''){
730
  if ($action!='new'){
731
    $o =& $this->objects[$id];
732
  }
733
  switch ($action){
734
    case 'new':
735
      if (is_array($options)){
736
        $this->objects[$id]=array('t'=>'action','info'=>$options,'type'=>$options['type']);
737
      } else {
738
        // then assume a URI action
739
        $this->objects[$id]=array('t'=>'action','info'=>$options,'type'=>'URI');
740
      }
741
      break;
742
    case 'out':
743
      if ($this->encrypted){
744
        $this->encryptInit($id);
745
      }
746
      $res="\n".$id." 0 obj\n<< /Type /Action";
747
      switch($o['type']){
748
        case 'ilink':
749
          // there will be an 'label' setting, this is the name of the destination
750
          $res.="\n/S /GoTo\n/D ".$this->destinations[(string)$o['info']['label']]." 0 R";
751
          break;
752
        case 'URI':
753
          $res.="\n/S /URI\n/URI (";
754
          if ($this->encrypted){
755
            $res.=$this->filterText($this->ARC4($o['info']));
756
          } else {
757
            $res.=$this->filterText($o['info']);
758
          }
759
          $res.=")";
760
          break;
761
      }
762
      $res.="\n>>\nendobj";
763
      return $res;
764
      break;
765
  }
766
}
767
 
768
/**
769
* an annotation object, this will add an annotation to the current page.
770
* initially will support just link annotations
771
*/
772
function o_annotation($id,$action,$options=''){
773
  if ($action!='new'){
774
    $o =& $this->objects[$id];
775
  }
776
  switch ($action){
777
    case 'new':
778
      // add the annotation to the current page
779
      $pageId = $this->currentPage;
780
      $this->o_page($pageId,'annot',$id);
781
      // and add the action object which is going to be required
782
      switch($options['type']){
783
        case 'link':
784
          $this->objects[$id]=array('t'=>'annotation','info'=>$options);
785
          $this->numObj++;
786
          $this->o_action($this->numObj,'new',$options['url']);
787
          $this->objects[$id]['info']['actionId']=$this->numObj;
788
          break;
789
        case 'ilink':
790
          // this is to a named internal link
791
          $label = $options['label'];
792
          $this->objects[$id]=array('t'=>'annotation','info'=>$options);
793
          $this->numObj++;
794
          $this->o_action($this->numObj,'new',array('type'=>'ilink','label'=>$label));
795
          $this->objects[$id]['info']['actionId']=$this->numObj;
796
          break;
797
      }
798
      break;
799
    case 'out':
800
      $res="\n".$id." 0 obj\n<< /Type /Annot";
801
      switch($o['info']['type']){
802
        case 'link':
803
        case 'ilink':
804
          $res.= "\n/Subtype /Link";
805
          break;
806
      }
807
      $res.="\n/A ".$o['info']['actionId']." 0 R";
808
      $res.="\n/Border [0 0 0]";
809
      $res.="\n/H /I";
810
      $res.="\n/Rect [ ";
811
      foreach($o['info']['rect'] as $v){
812
        $res.= sprintf("%.4f ",$v);
813
      }
814
      $res.="]";
815
      $res.="\n>>\nendobj";
816
      return $res;
817
      break;
818
  }
819
}
820
 
821
/**
822
* a page object, it also creates a contents object to hold its contents
823
*/
824
function o_page($id,$action,$options=''){
825
  if ($action!='new'){
826
    $o =& $this->objects[$id];
827
  }
828
  switch ($action){
829
    case 'new':
830
      $this->numPages++;
831
      $this->objects[$id]=array('t'=>'page','info'=>array('parent'=>$this->currentNode,'pageNum'=>$this->numPages));
832
      if (is_array($options)){
833
        // then this must be a page insertion, array shoudl contain 'rid','pos'=[before|after]
834
        $options['id']=$id;
835
        $this->o_pages($this->currentNode,'page',$options);
836
      } else {
837
        $this->o_pages($this->currentNode,'page',$id);
838
      }
839
      $this->currentPage=$id;
840
      //make a contents object to go with this page
841
      $this->numObj++;
842
      $this->o_contents($this->numObj,'new',$id);
843
      $this->currentContents=$this->numObj;
844
      $this->objects[$id]['info']['contents']=array();
845
      $this->objects[$id]['info']['contents'][]=$this->numObj;
846
      $match = ($this->numPages%2 ? 'odd' : 'even');
847
      foreach($this->addLooseObjects as $oId=>$target){
848
        if ($target=='all' || $match==$target){
849
          $this->objects[$id]['info']['contents'][]=$oId;
850
        }
851
      }
852
      break;
853
    case 'content':
854
      $o['info']['contents'][]=$options;
855
      break;
856
    case 'annot':
857
      // add an annotation to this page
858
      if (!isset($o['info']['annot'])){
859
        $o['info']['annot']=array();
860
      }
861
      // $options should contain the id of the annotation dictionary
862
      $o['info']['annot'][]=$options;
863
      break;
864
    case 'out':
865
      $res="\n".$id." 0 obj\n<< /Type /Page";
866
      $res.="\n/Parent ".$o['info']['parent']." 0 R";
867
      if (isset($o['info']['annot'])){
868
        $res.="\n/Annots [";
869
        foreach($o['info']['annot'] as $aId){
870
          $res.=" ".$aId." 0 R";
871
        }
872
        $res.=" ]";
873
      }
874
      $count = count($o['info']['contents']);
875
      if ($count==1){
876
        $res.="\n/Contents ".$o['info']['contents'][0]." 0 R";
877
      } else if ($count>1){
878
        $res.="\n/Contents [\n";
879
        foreach ($o['info']['contents'] as $cId){
880
          $res.=$cId." 0 R\n";
881
        }
882
        $res.="]";
883
      }
884
      $res.="\n>>\nendobj";
885
      return $res;
886
      break;
887
  }
888
}
889
 
890
/**
891
* the contents objects hold all of the content which appears on pages
892
*/
893
function o_contents($id,$action,$options=''){
894
  if ($action!='new'){
895
    $o =& $this->objects[$id];
896
  }
897
  switch ($action){
898
    case 'new':
899
      $this->objects[$id]=array('t'=>'contents','c'=>'','info'=>array());
900
      if (strlen($options) && intval($options)){
901
        // then this contents is the primary for a page
902
        $this->objects[$id]['onPage']=$options;
903
      } else if ($options=='raw'){
904
        // then this page contains some other type of system object
905
        $this->objects[$id]['raw']=1;
906
      }
907
      break;
908
    case 'add':
909
      // add more options to the decleration
910
      foreach ($options as $k=>$v){
911
        $o['info'][$k]=$v;
912
      }
913
    case 'out':
914
      $tmp=$o['c'];
915
      $res= "\n".$id." 0 obj\n";
916
      if (isset($this->objects[$id]['raw'])){
917
        $res.=$tmp;
918
      } else {
919
        $res.= "<<";
920
        if (function_exists('gzcompress') && $this->options['compression']){
921
          // then implement ZLIB based compression on this content stream
922
          $res.=" /Filter /FlateDecode";
923
          $tmp = gzcompress($tmp);
924
        }
925
        if ($this->encrypted){
926
          $this->encryptInit($id);
927
          $tmp = $this->ARC4($tmp);
928
        }
929
        foreach($o['info'] as $k=>$v){
930
          $res .= "\n/".$k.' '.$v;
931
        }
932
        $res.="\n/Length ".strlen($tmp)." >>\nstream\n".$tmp."\nendstream";
933
      }
934
      $res.="\nendobj\n";
935
      return $res;
936
      break;
937
  }
938
}
939
 
940
/**
941
* an image object, will be an XObject in the document, includes description and data
942
*/
943
function o_image($id,$action,$options=''){
944
  if ($action!='new'){
945
    $o =& $this->objects[$id];
946
  }
947
  switch($action){
948
    case 'new':
949
      // make the new object
950
      $this->objects[$id]=array('t'=>'image','data'=>$options['data'],'info'=>array());
951
      $this->objects[$id]['info']['Type']='/XObject';
952
      $this->objects[$id]['info']['Subtype']='/Image';
953
      $this->objects[$id]['info']['Width']=$options['iw'];
954
      $this->objects[$id]['info']['Height']=$options['ih'];
955
      if (!isset($options['type']) || $options['type']=='jpg'){
956
        if (!isset($options['channels'])){
957
          $options['channels']=3;
958
        }
959
        switch($options['channels']){
960
          case 1:
961
            $this->objects[$id]['info']['ColorSpace']='/DeviceGray';
962
            break;
963
          default:
964
            $this->objects[$id]['info']['ColorSpace']='/DeviceRGB';
965
            break;
966
        }
967
        $this->objects[$id]['info']['Filter']='/DCTDecode';
968
        $this->objects[$id]['info']['BitsPerComponent']=8;
969
      } else if ($options['type']=='png'){
970
        $this->objects[$id]['info']['Filter']='/FlateDecode';
971
        $this->objects[$id]['info']['DecodeParms']='<< /Predictor 15 /Colors '.$options['ncolor'].' /Columns '.$options['iw'].' /BitsPerComponent '.$options['bitsPerComponent'].'>>';
972
        if (strlen($options['pdata'])){
973
          $tmp = ' [ /Indexed /DeviceRGB '.(strlen($options['pdata'])/3-1).' ';
974
          $this->numObj++;
975
          $this->o_contents($this->numObj,'new');
976
          $this->objects[$this->numObj]['c']=$options['pdata'];
977
          $tmp.=$this->numObj.' 0 R';
978
          $tmp .=' ]';
979
          $this->objects[$id]['info']['ColorSpace'] = $tmp;
980
          if (isset($options['transparency'])){
981
            switch($options['transparency']['type']){
982
              case 'indexed':
983
                $tmp=' [ '.$options['transparency']['data'].' '.$options['transparency']['data'].'] ';
984
                $this->objects[$id]['info']['Mask'] = $tmp;
985
                break;
986
            }
987
          }
988
        } else {
989
          $this->objects[$id]['info']['ColorSpace']='/'.$options['color'];
990
        }
991
        $this->objects[$id]['info']['BitsPerComponent']=$options['bitsPerComponent'];
992
      }
993
      // assign it a place in the named resource dictionary as an external object, according to
994
      // the label passed in with it.
995
      $this->o_pages($this->currentNode,'xObject',array('label'=>$options['label'],'objNum'=>$id));
996
      // also make sure that we have the right procset object for it.
997
      $this->o_procset($this->procsetObjectId,'add','ImageC');
998
      break;
999
    case 'out':
1000
      $tmp=$o['data'];
1001
      $res= "\n".$id." 0 obj\n<<";
1002
      foreach($o['info'] as $k=>$v){
1003
        $res.="\n/".$k.' '.$v;
1004
      }
1005
      if ($this->encrypted){
1006
        $this->encryptInit($id);
1007
        $tmp = $this->ARC4($tmp);
1008
      }
1009
      $res.="\n/Length ".strlen($tmp)." >>\nstream\n".$tmp."\nendstream\nendobj\n";
1010
      return $res;
1011
      break;
1012
  }
1013
}
1014
 
1015
/**
1016
* encryption object.
1017
*/
1018
function o_encryption($id,$action,$options=''){
1019
  if ($action!='new'){
1020
    $o =& $this->objects[$id];
1021
  }
1022
  switch($action){
1023
    case 'new':
1024
      // make the new object
1025
      $this->objects[$id]=array('t'=>'encryption','info'=>$options);
1026
      $this->arc4_objnum=$id;
1027
      // figure out the additional paramaters required
1028
      $pad = chr(0x28).chr(0xBF).chr(0x4E).chr(0x5E).chr(0x4E).chr(0x75).chr(0x8A).chr(0x41).chr(0x64).chr(0x00).chr(0x4E).chr(0x56).chr(0xFF).chr(0xFA).chr(0x01).chr(0x08).chr(0x2E).chr(0x2E).chr(0x00).chr(0xB6).chr(0xD0).chr(0x68).chr(0x3E).chr(0x80).chr(0x2F).chr(0x0C).chr(0xA9).chr(0xFE).chr(0x64).chr(0x53).chr(0x69).chr(0x7A);
1029
      $len = strlen($options['owner']);
1030
      if ($len>32){
1031
        $owner = substr($options['owner'],0,32);
1032
      } else if ($len<32){
1033
        $owner = $options['owner'].substr($pad,0,32-$len);
1034
      } else {
1035
        $owner = $options['owner'];
1036
      }
1037
      $len = strlen($options['user']);
1038
      if ($len>32){
1039
        $user = substr($options['user'],0,32);
1040
      } else if ($len<32){
1041
        $user = $options['user'].substr($pad,0,32-$len);
1042
      } else {
1043
        $user = $options['user'];
1044
      }
1045
      $tmp = $this->md5_16($owner);
1046
      $okey = substr($tmp,0,5);
1047
      $this->ARC4_init($okey);
1048
      $ovalue=$this->ARC4($user);
1049
      $this->objects[$id]['info']['O']=$ovalue;
1050
      // now make the u value, phew.
1051
      $tmp = $this->md5_16($user.$ovalue.chr($options['p']).chr(255).chr(255).chr(255).$this->fileIdentifier);
1052
      $ukey = substr($tmp,0,5);
1053
 
1054
      $this->ARC4_init($ukey);
1055
      $this->encryptionKey = $ukey;
1056
      $this->encrypted=1;
1057
      $uvalue=$this->ARC4($pad);
1058
 
1059
      $this->objects[$id]['info']['U']=$uvalue;
1060
      $this->encryptionKey=$ukey;
1061
 
1062
      // initialize the arc4 array
1063
      break;
1064
    case 'out':
1065
      $res= "\n".$id." 0 obj\n<<";
1066
      $res.="\n/Filter /Standard";
1067
      $res.="\n/V 1";
1068
      $res.="\n/R 2";
1069
      $res.="\n/O (".$this->filterText($o['info']['O']).')';
1070
      $res.="\n/U (".$this->filterText($o['info']['U']).')';
1071
      // and the p-value needs to be converted to account for the twos-complement approach
1072
      $o['info']['p'] = (($o['info']['p']^255)+1)*-1;
1073
      $res.="\n/P ".($o['info']['p']);
1074
      $res.="\n>>\nendobj\n";
1075
 
1076
      return $res;
1077
      break;
1078
  }
1079
}
1080
 
1081
/**
1082
* ARC4 functions
1083
* A series of function to implement ARC4 encoding in PHP
1084
*/
1085
 
1086
/**
1087
* calculate the 16 byte version of the 128 bit md5 digest of the string
1088
*/
1089
function md5_16($string){
1090
  $tmp = md5($string);
1091
  $out='';
1092
  for ($i=0;$i<=30;$i=$i+2){
1093
    $out.=chr(hexdec(substr($tmp,$i,2)));
1094
  }
1095
  return $out;
1096
}
1097
 
1098
/**
1099
* initialize the encryption for processing a particular object
1100
*/
1101
function encryptInit($id){
1102
  $tmp = $this->encryptionKey;
1103
  $hex = dechex($id);
1104
  if (strlen($hex)<6){
1105
    $hex = substr('000000',0,6-strlen($hex)).$hex;
1106
  }
1107
  $tmp.= chr(hexdec(substr($hex,4,2))).chr(hexdec(substr($hex,2,2))).chr(hexdec(substr($hex,0,2))).chr(0).chr(0);
1108
  $key = $this->md5_16($tmp);
1109
  $this->ARC4_init(substr($key,0,10));
1110
}
1111
 
1112
/**
1113
* initialize the ARC4 encryption
1114
*/
1115
function ARC4_init($key=''){
1116
  $this->arc4 = '';
1117
  // setup the control array
1118
  if (strlen($key)==0){
1119
    return;
1120
  }
1121
  $k = '';
1122
  while(strlen($k)<256){
1123
    $k.=$key;
1124
  }
1125
  $k=substr($k,0,256);
1126
  for ($i=0;$i<256;$i++){
1127
    $this->arc4 .= chr($i);
1128
  }
1129
  $j=0;
1130
  for ($i=0;$i<256;$i++){
1131
    $t = $this->arc4[$i];
1132
    $j = ($j + ord($t) + ord($k[$i]))%256;
1133
    $this->arc4[$i]=$this->arc4[$j];
1134
    $this->arc4[$j]=$t;
1135
  }
1136
}
1137
 
1138
/**
1139
* ARC4 encrypt a text string
1140
*/
1141
function ARC4($text){
1142
  $len=strlen($text);
1143
  $a=0;
1144
  $b=0;
1145
  $c = $this->arc4;
1146
  $out='';
1147
  for ($i=0;$i<$len;$i++){
1148
    $a = ($a+1)%256;
1149
    $t= $c[$a];
1150
    $b = ($b+ord($t))%256;
1151
    $c[$a]=$c[$b];
1152
    $c[$b]=$t;
1153
    $k = ord($c[(ord($c[$a])+ord($c[$b]))%256]);
1154
    $out.=chr(ord($text[$i]) ^ $k);
1155
  }
1156
 
1157
  return $out;
1158
}
1159
 
1160
/**
1161
* functions which can be called to adjust or add to the document
1162
*/
1163
 
1164
/**
1165
* add a link in the document to an external URL
1166
*/
1167
function addLink($url,$x0,$y0,$x1,$y1){
1168
  $this->numObj++;
1169
  $info = array('type'=>'link','url'=>$url,'rect'=>array($x0,$y0,$x1,$y1));
1170
  $this->o_annotation($this->numObj,'new',$info);
1171
}
1172
 
1173
/**
1174
* add a link in the document to an internal destination (ie. within the document)
1175
*/
1176
function addInternalLink($label,$x0,$y0,$x1,$y1){
1177
  $this->numObj++;
1178
  $info = array('type'=>'ilink','label'=>$label,'rect'=>array($x0,$y0,$x1,$y1));
1179
  $this->o_annotation($this->numObj,'new',$info);
1180
}
1181
 
1182
/**
1183
* set the encryption of the document
1184
* can be used to turn it on and/or set the passwords which it will have.
1185
* also the functions that the user will have are set here, such as print, modify, add
1186
*/
1187
function setEncryption($userPass='',$ownerPass='',$pc=array()){
1188
  $p=bindec(11000000);
1189
 
1190
  $options = array(
1191
     'print'=>4
1192
    ,'modify'=>8
1193
    ,'copy'=>16
1194
    ,'add'=>32
1195
  );
1196
  foreach($pc as $k=>$v){
1197
    if ($v && isset($options[$k])){
1198
      $p+=$options[$k];
1199
    } else if (isset($options[$v])){
1200
      $p+=$options[$v];
1201
    }
1202
  }
1203
  // implement encryption on the document
1204
  if ($this->arc4_objnum == 0){
1205
    // then the block does not exist already, add it.
1206
    $this->numObj++;
1207
    if (strlen($ownerPass)==0){
1208
      $ownerPass=$userPass;
1209
    }
1210
    $this->o_encryption($this->numObj,'new',array('user'=>$userPass,'owner'=>$ownerPass,'p'=>$p));
1211
  }
1212
}
1213
 
1214
/**
1215
* should be used for internal checks, not implemented as yet
1216
*/
1217
function checkAllHere(){
1218
}
1219
 
1220
/**
1221
* return the pdf stream as a string returned from the function
1222
*/
1223
function output($debug=0){
1224
 
1225
  if ($debug){
1226
    // turn compression off
1227
    $this->options['compression']=0;
1228
  }
1229
 
1230
  if ($this->arc4_objnum){
1231
    $this->ARC4_init($this->encryptionKey);
1232
  }
1233
 
1234
  $this->checkAllHere();
1235
 
1236
  $xref=array();
1237
  $content="%PDF-1.3\n%âãÏÓ\n";
1238
//  $content="%PDF-1.3\n";
1239
  $pos=strlen($content);
1240
  foreach($this->objects as $k=>$v){
1241
    $tmp='o_'.$v['t'];
1242
    $cont=$this->$tmp($k,'out');
1243
    $content.=$cont;
1244
    $xref[]=$pos;
1245
    $pos+=strlen($cont);
1246
  }
1247
  $content.="\nxref\n0 ".(count($xref)+1)."\n0000000000 65535 f \n";
1248
  foreach($xref as $p){
1249
    $content.=substr('0000000000',0,10-strlen($p)).$p." 00000 n \n";
1250
  }
1251
  $content.="\ntrailer\n  << /Size ".(count($xref)+1)."\n     /Root 1 0 R\n     /Info ".$this->infoObject." 0 R\n";
1252
  // if encryption has been applied to this document then add the marker for this dictionary
1253
  if ($this->arc4_objnum > 0){
1254
    $content .= "/Encrypt ".$this->arc4_objnum." 0 R\n";
1255
  }
1256
  if (strlen($this->fileIdentifier)){
1257
    $content .= "/ID[<".$this->fileIdentifier."><".$this->fileIdentifier.">]\n";
1258
  }
1259
  $content .= "  >>\nstartxref\n".$pos."\n%%EOF\n";
1260
  return $content;
1261
}
1262
 
1263
/**
1264
* intialize a new document
1265
* if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum
1266
* this function is called automatically by the constructor function
1267
*
1268
* @access private
1269
*/
1270
function newDocument($pageSize=array(0,0,612,792)){
1271
  $this->numObj=0;
1272
  $this->objects = array();
1273
 
1274
  $this->numObj++;
1275
  $this->o_catalog($this->numObj,'new');
1276
 
1277
  $this->numObj++;
1278
  $this->o_outlines($this->numObj,'new');
1279
 
1280
  $this->numObj++;
1281
  $this->o_pages($this->numObj,'new');
1282
 
1283
  $this->o_pages($this->numObj,'mediaBox',$pageSize);
1284
  $this->currentNode = 3;
1285
 
1286
  $this->numObj++;
1287
  $this->o_procset($this->numObj,'new');
1288
 
1289
  $this->numObj++;
1290
  $this->o_info($this->numObj,'new');
1291
 
1292
  $this->numObj++;
1293
  $this->o_page($this->numObj,'new');
1294
 
1295
  // need to store the first page id as there is no way to get it to the user during
1296
  // startup
1297
  $this->firstPageId = $this->currentContents;
1298
}
1299
 
1300
/**
1301
* open the font file and return a php structure containing it.
1302
* first check if this one has been done before and saved in a form more suited to php
1303
* note that if a php serialized version does not exist it will try and make one, but will
1304
* require write access to the directory to do it... it is MUCH faster to have these serialized
1305
* files.
1306
*
1307
* @access private
1308
*/
1309
function openFont($font){
1310
  // assume that $font contains both the path and perhaps the extension to the file, split them
1311
  $pos=strrpos($font,'/');
1312
  if ($pos===false){
1313
    $dir = './';
1314
    $name = $font;
1315
  } else {
1316
    $dir=substr($font,0,$pos+1);
1317
    $name=substr($font,$pos+1);
1318
  }
1319
 
1320
  if (substr($name,-4)=='.afm'){
1321
    $name=substr($name,0,strlen($name)-4);
1322
  }
1323
  $this->addMessage('openFont: '.$font.' - '.$name);
1324
  if (file_exists($dir.'php_'.$name.'.afm')){
1325
    $this->addMessage('openFont: php file exists '.$dir.'php_'.$name.'.afm');
1326
    $tmp = file($dir.'php_'.$name.'.afm');
1327
    $this->fonts[$font]=unserialize($tmp[0]);
1328
    if (!isset($this->fonts[$font]['_version_']) || $this->fonts[$font]['_version_']<1){
1329
      // if the font file is old, then clear it out and prepare for re-creation
1330
      $this->addMessage('openFont: clear out, make way for new version.');
1331
      unset($this->fonts[$font]);
1332
    }
1333
  }
1334
  if (!isset($this->fonts[$font]) && file_exists($dir.$name.'.afm')){
1335
    // then rebuild the php_<font>.afm file from the <font>.afm file
1336
    $this->addMessage('openFont: build php file from '.$dir.$name.'.afm');
1337
    $data = array();
1338
    $file = file($dir.$name.'.afm');
1339
    foreach ($file as $rowA){
1340
      $row=trim($rowA);
1341
      $pos=strpos($row,' ');
1342
      if ($pos){
1343
        // then there must be some keyword
1344
        $key = substr($row,0,$pos);
1345
        switch ($key){
1346
          case 'FontName':
1347
          case 'FullName':
1348
          case 'FamilyName':
1349
          case 'Weight':
1350
          case 'ItalicAngle':
1351
          case 'IsFixedPitch':
1352
          case 'CharacterSet':
1353
          case 'UnderlinePosition':
1354
          case 'UnderlineThickness':
1355
          case 'Version':
1356
          case 'EncodingScheme':
1357
          case 'CapHeight':
1358
          case 'XHeight':
1359
          case 'Ascender':
1360
          case 'Descender':
1361
          case 'StdHW':
1362
          case 'StdVW':
1363
          case 'StartCharMetrics':
1364
            $data[$key]=trim(substr($row,$pos));
1365
            break;
1366
          case 'FontBBox':
1367
            $data[$key]=explode(' ',trim(substr($row,$pos)));
1368
            break;
1369
          case 'C':
1370
            //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ;
1371
            $bits=explode(';',trim($row));
1372
            $dtmp=array();
1373
            foreach($bits as $bit){
1374
              $bits2 = explode(' ',trim($bit));
1375
              if (strlen($bits2[0])){
1376
                if (count($bits2)>2){
1377
                  $dtmp[$bits2[0]]=array();
1378
                  for ($i=1;$i<count($bits2);$i++){
1379
                    $dtmp[$bits2[0]][]=$bits2[$i];
1380
                  }
1381
                } else if (count($bits2)==2){
1382
                  $dtmp[$bits2[0]]=$bits2[1];
1383
                }
1384
              }
1385
            }
1386
            if ($dtmp['C']>=0){
1387
              $data['C'][$dtmp['C']]=$dtmp;
1388
              $data['C'][$dtmp['N']]=$dtmp;
1389
            } else {
1390
              $data['C'][$dtmp['N']]=$dtmp;
1391
            }
1392
            break;
1393
          case 'KPX':
1394
            //KPX Adieresis yacute -40
1395
            $bits=explode(' ',trim($row));
1396
            $data['KPX'][$bits[1]][$bits[2]]=$bits[3];
1397
            break;
1398
        }
1399
      }
1400
    }
1401
    $data['_version_']=1;
1402
    $this->fonts[$font]=$data;
1403
    $fp = fopen($dir.'php_'.$name.'.afm','w');
1404
    fwrite($fp,serialize($data));
1405
    fclose($fp);
1406
  } else if (!isset($this->fonts[$font])){
1407
    $this->addMessage('openFont: no font file found');
1408
//    echo 'Font not Found '.$font;
1409
  }
1410
}
1411
 
1412
/**
1413
* if the font is not loaded then load it and make the required object
1414
* else just make it the current font
1415
* the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding'
1416
* note that encoding='none' will need to be used for symbolic fonts
1417
* and 'differences' => an array of mappings between numbers 0->255 and character names.
1418
*
1419
*/
1420
function selectFont($fontName,$encoding='',$set=1){
1421
  if (!isset($this->fonts[$fontName])){
1422
    // load the file
1423
    $this->openFont($fontName);
1424
    if (isset($this->fonts[$fontName])){
1425
      $this->numObj++;
1426
      $this->numFonts++;
1427
      $pos=strrpos($fontName,'/');
1428
//      $dir=substr($fontName,0,$pos+1);
1429
      $name=substr($fontName,$pos+1);
1430
      if (substr($name,-4)=='.afm'){
1431
        $name=substr($name,0,strlen($name)-4);
1432
      }
1433
      $options=array('name'=>$name);
1434
      if (is_array($encoding)){
1435
        // then encoding and differences might be set
1436
        if (isset($encoding['encoding'])){
1437
          $options['encoding']=$encoding['encoding'];
1438
        }
1439
        if (isset($encoding['differences'])){
1440
          $options['differences']=$encoding['differences'];
1441
        }
1442
      } else if (strlen($encoding)){
1443
        // then perhaps only the encoding has been set
1444
        $options['encoding']=$encoding;
1445
      }
1446
      $fontObj = $this->numObj;
1447
      $this->o_font($this->numObj,'new',$options);
1448
      $this->fonts[$fontName]['fontNum']=$this->numFonts;
1449
      // if this is a '.afm' font, and there is a '.pfa' file to go with it ( as there
1450
      // should be for all non-basic fonts), then load it into an object and put the
1451
      // references into the font object
1452
      $basefile = substr($fontName,0,strlen($fontName)-4);
1453
      if (file_exists($basefile.'.pfb')){
1454
        $fbtype = 'pfb';
1455
      } else if (file_exists($basefile.'.ttf')){
1456
        $fbtype = 'ttf';
1457
      } else {
1458
        $fbtype='';
1459
      }
1460
      $fbfile = $basefile.'.'.$fbtype;
1461
 
1462
//      $pfbfile = substr($fontName,0,strlen($fontName)-4).'.pfb';
1463
//      $ttffile = substr($fontName,0,strlen($fontName)-4).'.ttf';
1464
      $this->addMessage('selectFont: checking for - '.$fbfile);
1465
      if (substr($fontName,-4)=='.afm' && strlen($fbtype) ){
1466
        $adobeFontName = $this->fonts[$fontName]['FontName'];
1467
//        $fontObj = $this->numObj;
1468
        $this->addMessage('selectFont: adding font file - '.$fbfile.' - '.$adobeFontName);
1469
        // find the array of fond widths, and put that into an object.
1470
        $firstChar = -1;
1471
        $lastChar = 0;
1472
        $widths = array();
1473
        foreach ($this->fonts[$fontName]['C'] as $num=>$d){
1474
          if (intval($num)>0 || $num=='0'){
1475
            if ($lastChar>0 && $num>$lastChar+1){
1476
              for($i=$lastChar+1;$i<$num;$i++){
1477
                $widths[] = 0;
1478
              }
1479
            }
1480
            $widths[] = $d['WX'];
1481
            if ($firstChar==-1){
1482
              $firstChar = $num;
1483
            }
1484
            $lastChar = $num;
1485
          }
1486
        }
1487
        // also need to adjust the widths for the differences array
1488
        if (isset($options['differences'])){
1489
          foreach($options['differences'] as $charNum=>$charName){
1490
            if ($charNum>$lastChar){
1491
              for($i=$lastChar+1;$i<=$charNum;$i++){
1492
                $widths[]=0;
1493
              }
1494
              $lastChar=$charNum;
1495
            }
1496
            if (isset($this->fonts[$fontName]['C'][$charName])){
1497
              $widths[$charNum-$firstChar]=$this->fonts[$fontName]['C'][$charName]['WX'];
1498
            }
1499
          }
1500
        }
1501
        $this->addMessage('selectFont: FirstChar='.$firstChar);
1502
        $this->addMessage('selectFont: LastChar='.$lastChar);
1503
        $this->numObj++;
1504
        $this->o_contents($this->numObj,'new','raw');
1505
        $this->objects[$this->numObj]['c'].='[';
1506
        foreach($widths as $width){
1507
          $this->objects[$this->numObj]['c'].=' '.$width;
1508
        }
1509
        $this->objects[$this->numObj]['c'].=' ]';
1510
        $widthid = $this->numObj;
1511
 
1512
        // load the pfb file, and put that into an object too.
1513
        // note that pdf supports only binary format type 1 font files, though there is a
1514
        // simple utility to convert them from pfa to pfb.
1515
        $fp = fopen($fbfile,'rb');
1516
        $tmp = get_magic_quotes_runtime();
1517
        set_magic_quotes_runtime(0);
1518
        $data = fread($fp,filesize($fbfile));
1519
        set_magic_quotes_runtime($tmp);
1520
        fclose($fp);
1521
 
1522
        // create the font descriptor
1523
        $this->numObj++;
1524
        $fontDescriptorId = $this->numObj;
1525
        $this->numObj++;
1526
        $pfbid = $this->numObj;
1527
        // determine flags (more than a little flakey, hopefully will not matter much)
1528
        $flags=0;
1529
        if ($this->fonts[$fontName]['ItalicAngle']!=0){ $flags+=pow(2,6); }
1530
        if ($this->fonts[$fontName]['IsFixedPitch']=='true'){ $flags+=1; }
1531
        $flags+=pow(2,5); // assume non-sybolic
1532
 
1533
        $list = array('Ascent'=>'Ascender','CapHeight'=>'CapHeight','Descent'=>'Descender','FontBBox'=>'FontBBox','ItalicAngle'=>'ItalicAngle');
1534
        $fdopt = array(
1535
         'Flags'=>$flags
1536
         ,'FontName'=>$adobeFontName
1537
         ,'StemV'=>100  // don't know what the value for this should be!
1538
        );
1539
        foreach($list as $k=>$v){
1540
          if (isset($this->fonts[$fontName][$v])){
1541
            $fdopt[$k]=$this->fonts[$fontName][$v];
1542
          }
1543
        }
1544
 
1545
        if ($fbtype=='pfb'){
1546
          $fdopt['FontFile']=$pfbid;
1547
        } else if ($fbtype=='ttf'){
1548
          $fdopt['FontFile2']=$pfbid;
1549
        }
1550
        $this->o_fontDescriptor($fontDescriptorId,'new',$fdopt);
1551
 
1552
        // embed the font program
1553
        $this->o_contents($this->numObj,'new');
1554
        $this->objects[$pfbid]['c'].=$data;
1555
        // determine the cruicial lengths within this file
1556
        if ($fbtype=='pfb'){
1557
          $l1 = strpos($data,'eexec')+6;
1558
          $l2 = strpos($data,'00000000')-$l1;
1559
          $l3 = strlen($data)-$l2-$l1;
1560
          $this->o_contents($this->numObj,'add',array('Length1'=>$l1,'Length2'=>$l2,'Length3'=>$l3));
1561
        } else if ($fbtype=='ttf'){
1562
          $l1 = strlen($data);
1563
          $this->o_contents($this->numObj,'add',array('Length1'=>$l1));
1564
        }
1565
 
1566
 
1567
        // tell the font object about all this new stuff
1568
        $tmp = array('BaseFont'=>$adobeFontName,'Widths'=>$widthid
1569
                                      ,'FirstChar'=>$firstChar,'LastChar'=>$lastChar
1570
                                      ,'FontDescriptor'=>$fontDescriptorId);
1571
        if ($fbtype=='ttf'){
1572
          $tmp['SubType']='TrueType';
1573
        }
1574
        $this->addMessage('adding extra info to font.('.$fontObj.')');
1575
        foreach($tmp as $fk=>$fv){
1576
          $this->addMessage($fk." : ".$fv);
1577
        }
1578
        $this->o_font($fontObj,'add',$tmp);
1579
 
1580
      } else {
1581
        $this->addMessage('selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts');
1582
      }
1583
 
1584
 
1585
      // also set the differences here, note that this means that these will take effect only the
1586
      //first time that a font is selected, else they are ignored
1587
      if (isset($options['differences'])){
1588
        $this->fonts[$fontName]['differences']=$options['differences'];
1589
      }
1590
    }
1591
  }
1592
  if ($set && isset($this->fonts[$fontName])){
1593
    // so if for some reason the font was not set in the last one then it will not be selected
1594
    $this->currentBaseFont=$fontName;
1595
    // the next line means that if a new font is selected, then the current text state will be
1596
    // applied to it as well.
1597
    $this->setCurrentFont();
1598
  }
1599
  return $this->currentFontNum;
1600
}
1601
 
1602
/**
1603
* sets up the current font, based on the font families, and the current text state
1604
* note that this system is quite flexible, a <<b>><<i>> font can be completely different to a
1605
* <<i>><<b>> font, and even <<b>><<b>> will have to be defined within the family to have meaning
1606
* This function is to be called whenever the currentTextState is changed, it will update
1607
* the currentFont setting to whatever the appropriatte family one is.
1608
* If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont
1609
* This function will change the currentFont to whatever it should be, but will not change the
1610
* currentBaseFont.
1611
*
1612
* @access private
1613
*/
1614
function setCurrentFont(){
1615
  if (strlen($this->currentBaseFont)==0){
1616
    // then assume an initial font
1617
    $this->selectFont('./fonts/Helvetica.afm');
1618
  }
1619
  $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1);
1620
  if (strlen($this->currentTextState)
1621
    && isset($this->fontFamilies[$cf])
1622
      && isset($this->fontFamilies[$cf][$this->currentTextState])){
1623
    // then we are in some state or another
1624
    // and this font has a family, and the current setting exists within it
1625
    // select the font, then return it
1626
    $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState];
1627
    $this->selectFont($nf,'',0);
1628
    $this->currentFont = $nf;
1629
    $this->currentFontNum = $this->fonts[$nf]['fontNum'];
1630
  } else {
1631
    // the this font must not have the right family member for the current state
1632
    // simply assume the base font
1633
    $this->currentFont = $this->currentBaseFont;
1634
    $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum'];
1635
  }
1636
}
1637
 
1638
/**
1639
* function for the user to find out what the ID is of the first page that was created during
1640
* startup - useful if they wish to add something to it later.
1641
*/
1642
function getFirstPageId(){
1643
  return $this->firstPageId;
1644
}
1645
 
1646
/**
1647
* add content to the currently active object
1648
*
1649
* @access private
1650
*/
1651
function addContent($content){
1652
  $this->objects[$this->currentContents]['c'].=$content;
1653
}
1654
 
1655
/**
1656
* sets the colour for fill operations
1657
*/
1658
function setColor($r,$g,$b,$force=0){
1659
  if ($r>=0 && ($force || $r!=$this->currentColour['r'] || $g!=$this->currentColour['g'] || $b!=$this->currentColour['b'])){
1660
    $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$r).' '.sprintf('%.3f',$g).' '.sprintf('%.3f',$b).' rg';
1661
    $this->currentColour=array('r'=>$r,'g'=>$g,'b'=>$b);
1662
  }
1663
}
1664
 
1665
/**
1666
* sets the colour for stroke operations
1667
*/
1668
function setStrokeColor($r,$g,$b,$force=0){
1669
  if ($r>=0 && ($force || $r!=$this->currentStrokeColour['r'] || $g!=$this->currentStrokeColour['g'] || $b!=$this->currentStrokeColour['b'])){
1670
    $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$r).' '.sprintf('%.3f',$g).' '.sprintf('%.3f',$b).' RG';
1671
    $this->currentStrokeColour=array('r'=>$r,'g'=>$g,'b'=>$b);
1672
  }
1673
}
1674
 
1675
/**
1676
* draw a line from one set of coordinates to another
1677
*/
1678
function line($x1,$y1,$x2,$y2){
1679
  $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' m '.sprintf('%.3f',$x2).' '.sprintf('%.3f',$y2).' l S';
1680
}
1681
 
1682
/**
1683
* draw a bezier curve based on 4 control points
1684
*/
1685
function curve($x0,$y0,$x1,$y1,$x2,$y2,$x3,$y3){
1686
  // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points
1687
  // as the control points for the curve.
1688
  $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x0).' '.sprintf('%.3f',$y0).' m '.sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1);
1689
  $this->objects[$this->currentContents]['c'].= ' '.sprintf('%.3f',$x2).' '.sprintf('%.3f',$y2).' '.sprintf('%.3f',$x3).' '.sprintf('%.3f',$y3).' c S';
1690
}
1691
 
1692
/**
1693
* draw a part of an ellipse
1694
*/
1695
function partEllipse($x0,$y0,$astart,$afinish,$r1,$r2=0,$angle=0,$nSeg=8){
1696
  $this->ellipse($x0,$y0,$r1,$r2,$angle,$nSeg,$astart,$afinish,0);
1697
}
1698
 
1699
/**
1700
* draw a filled ellipse
1701
*/
1702
function filledEllipse($x0,$y0,$r1,$r2=0,$angle=0,$nSeg=8,$astart=0,$afinish=360){
1703
  return $this->ellipse($x0,$y0,$r1,$r2=0,$angle,$nSeg,$astart,$afinish,1,1);
1704
}
1705
 
1706
/**
1707
* draw an ellipse
1708
* note that the part and filled ellipse are just special cases of this function
1709
*
1710
* draws an ellipse in the current line style
1711
* centered at $x0,$y0, radii $r1,$r2
1712
* if $r2 is not set, then a circle is drawn
1713
* nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a
1714
* pretty crappy shape at 2, as we are approximating with bezier curves.
1715
*/
1716
function ellipse($x0,$y0,$r1,$r2=0,$angle=0,$nSeg=8,$astart=0,$afinish=360,$close=1,$fill=0){
1717
  if ($r1==0){
1718
    return;
1719
  }
1720
  if ($r2==0){
1721
    $r2=$r1;
1722
  }
1723
  if ($nSeg<2){
1724
    $nSeg=2;
1725
  }
1726
 
1727
  $astart = deg2rad((float)$astart);
1728
  $afinish = deg2rad((float)$afinish);
1729
  $totalAngle =$afinish-$astart;
1730
 
1731
  $dt = $totalAngle/$nSeg;
1732
  $dtm = $dt/3;
1733
 
1734
  if ($angle != 0){
1735
    $a = -1*deg2rad((float)$angle);
1736
    $tmp = "\n q ";
1737
    $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' ';
1738
    $tmp .= sprintf('%.3f',$x0).' '.sprintf('%.3f',$y0).' cm';
1739
    $this->objects[$this->currentContents]['c'].= $tmp;
1740
    $x0=0;
1741
    $y0=0;
1742
  }
1743
 
1744
  $t1 = $astart;
1745
  $a0 = $x0+$r1*cos($t1);
1746
  $b0 = $y0+$r2*sin($t1);
1747
  $c0 = -$r1*sin($t1);
1748
  $d0 = $r2*cos($t1);
1749
 
1750
  $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$a0).' '.sprintf('%.3f',$b0).' m ';
1751
  for ($i=1;$i<=$nSeg;$i++){
1752
    // draw this bit of the total curve
1753
    $t1 = $i*$dt+$astart;
1754
    $a1 = $x0+$r1*cos($t1);
1755
    $b1 = $y0+$r2*sin($t1);
1756
    $c1 = -$r1*sin($t1);
1757
    $d1 = $r2*cos($t1);
1758
    $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',($a0+$c0*$dtm)).' '.sprintf('%.3f',($b0+$d0*$dtm));
1759
    $this->objects[$this->currentContents]['c'].= ' '.sprintf('%.3f',($a1-$c1*$dtm)).' '.sprintf('%.3f',($b1-$d1*$dtm)).' '.sprintf('%.3f',$a1).' '.sprintf('%.3f',$b1).' c';
1760
    $a0=$a1;
1761
    $b0=$b1;
1762
    $c0=$c1;
1763
    $d0=$d1;
1764
  }
1765
  if ($fill){
1766
    $this->objects[$this->currentContents]['c'].=' f';
1767
  } else {
1768
    if ($close){
1769
      $this->objects[$this->currentContents]['c'].=' s'; // small 's' signifies closing the path as well
1770
    } else {
1771
      $this->objects[$this->currentContents]['c'].=' S';
1772
    }
1773
  }
1774
  if ($angle !=0){
1775
    $this->objects[$this->currentContents]['c'].=' Q';
1776
  }
1777
}
1778
 
1779
/**
1780
* this sets the line drawing style.
1781
* width, is the thickness of the line in user units
1782
* cap is the type of cap to put on the line, values can be 'butt','round','square'
1783
*    where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the
1784
*    end of the line.
1785
* join can be 'miter', 'round', 'bevel'
1786
* dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the
1787
*   on and off dashes.
1788
*   (2) represents 2 on, 2 off, 2 on , 2 off ...
1789
*   (2,1) is 2 on, 1 off, 2 on, 1 off.. etc
1790
* phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts.
1791
*/
1792
function setLineStyle($width=1,$cap='',$join='',$dash='',$phase=0){
1793
 
1794
  // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day
1795
  $string = '';
1796
  if ($width>0){
1797
    $string.= $width.' w';
1798
  }
1799
  $ca = array('butt'=>0,'round'=>1,'square'=>2);
1800
  if (isset($ca[$cap])){
1801
    $string.= ' '.$ca[$cap].' J';
1802
  }
1803
  $ja = array('miter'=>0,'round'=>1,'bevel'=>2);
1804
  if (isset($ja[$join])){
1805
    $string.= ' '.$ja[$join].' j';
1806
  }
1807
  if (is_array($dash)){
1808
    $string.= ' [';
1809
    foreach ($dash as $len){
1810
      $string.=' '.$len;
1811
    }
1812
    $string.= ' ] '.$phase.' d';
1813
  }
1814
  $this->currentLineStyle = $string;
1815
  $this->objects[$this->currentContents]['c'].="\n".$string;
1816
}
1817
 
1818
/**
1819
* draw a polygon, the syntax for this is similar to the GD polygon command
1820
*/
1821
function polygon($p,$np,$f=0){
1822
  $this->objects[$this->currentContents]['c'].="\n";
1823
  $this->objects[$this->currentContents]['c'].=sprintf('%.3f',$p[0]).' '.sprintf('%.3f',$p[1]).' m ';
1824
  for ($i=2;$i<$np*2;$i=$i+2){
1825
    $this->objects[$this->currentContents]['c'].= sprintf('%.3f',$p[$i]).' '.sprintf('%.3f',$p[$i+1]).' l ';
1826
  }
1827
  if ($f==1){
1828
    $this->objects[$this->currentContents]['c'].=' f';
1829
  } else {
1830
    $this->objects[$this->currentContents]['c'].=' S';
1831
  }
1832
}
1833
 
1834
/**
1835
* a filled rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not
1836
* the coordinates of the upper-right corner
1837
*/
1838
function filledRectangle($x1,$y1,$width,$height){
1839
  $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' '.sprintf('%.3f',$width).' '.sprintf('%.3f',$height).' re f';
1840
}
1841
 
1842
/**
1843
* draw a rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not
1844
* the coordinates of the upper-right corner
1845
*/
1846
function rectangle($x1,$y1,$width,$height){
1847
  $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' '.sprintf('%.3f',$width).' '.sprintf('%.3f',$height).' re S';
1848
}
1849
 
1850
/**
1851
* add a new page to the document
1852
* this also makes the new page the current active object
1853
*/
1854
function newPage($insert=0,$id=0,$pos='after'){
1855
 
1856
  // if there is a state saved, then go up the stack closing them
1857
  // then on the new page, re-open them with the right setings
1858
 
1859
  if ($this->nStateStack){
1860
    for ($i=$this->nStateStack;$i>=1;$i--){
1861
      $this->restoreState($i);
1862
    }
1863
  }
1864
 
1865
  $this->numObj++;
1866
  if ($insert){
1867
    // the id from the ezPdf class is the od of the contents of the page, not the page object itself
1868
    // query that object to find the parent
1869
    $rid = $this->objects[$id]['onPage'];
1870
    $opt= array('rid'=>$rid,'pos'=>$pos);
1871
    $this->o_page($this->numObj,'new',$opt);
1872
  } else {
1873
    $this->o_page($this->numObj,'new');
1874
  }
1875
  // if there is a stack saved, then put that onto the page
1876
  if ($this->nStateStack){
1877
    for ($i=1;$i<=$this->nStateStack;$i++){
1878
      $this->saveState($i);
1879
    }
1880
  }
1881
  // and if there has been a stroke or fill colour set, then transfer them
1882
  if ($this->currentColour['r']>=0){
1883
    $this->setColor($this->currentColour['r'],$this->currentColour['g'],$this->currentColour['b'],1);
1884
  }
1885
  if ($this->currentStrokeColour['r']>=0){
1886
    $this->setStrokeColor($this->currentStrokeColour['r'],$this->currentStrokeColour['g'],$this->currentStrokeColour['b'],1);
1887
  }
1888
 
1889
  // if there is a line style set, then put this in too
1890
  if (strlen($this->currentLineStyle)){
1891
    $this->objects[$this->currentContents]['c'].="\n".$this->currentLineStyle;
1892
  }
1893
 
1894
  // the call to the o_page object set currentContents to the present page, so this can be returned as the page id
1895
  return $this->currentContents;
1896
}
1897
 
1898
/**
1899
* output the pdf code, streaming it to the browser
1900
* the relevant headers are set so that hopefully the browser will recognise it
1901
*/
1902
function stream($options=''){
1903
  // setting the options allows the adjustment of the headers
1904
  // values at the moment are:
1905
  // 'Content-Disposition'=>'filename'  - sets the filename, though not too sure how well this will
1906
  //        work as in my trial the browser seems to use the filename of the php file with .pdf on the end
1907
  // 'Accept-Ranges'=>1 or 0 - if this is not set to 1, then this header is not included, off by default
1908
  //    this header seems to have caused some problems despite tha fact that it is supposed to solve
1909
  //    them, so I am leaving it off by default.
1910
  // 'compress'=> 1 or 0 - apply content stream compression, this is on (1) by default
1911
  if (!is_array($options)){
1912
    $options=array();
1913
  }
1914
  if ( isset($options['compress']) && $options['compress']==0){
1915
    $tmp = $this->output(1);
1916
  } else {
1917
    $tmp = $this->output();
1918
  }
1919
  header("Content-type: application/pdf");
1920
  header("Content-Length: ".strlen(ltrim($tmp)));
1921
  $fileName = (isset($options['Content-Disposition'])?$options['Content-Disposition']:'file.pdf');
1922
  header("Content-Disposition: inline; filename=".$fileName);
1923
  if (isset($options['Accept-Ranges']) && $options['Accept-Ranges']==1){
1924
    header("Accept-Ranges: ".strlen(ltrim($tmp)));
1925
  }
1926
  echo ltrim($tmp);
1927
}
1928
 
1929
/**
1930
* return the height in units of the current font in the given size
1931
*/
1932
function getFontHeight($size){
1933
  if (!$this->numFonts){
1934
    $this->selectFont('./fonts/Helvetica');
1935
  }
1936
  // for the current font, and the given size, what is the height of the font in user units
1937
  $h = $this->fonts[$this->currentFont]['FontBBox'][3]-$this->fonts[$this->currentFont]['FontBBox'][1];
1938
  return $size*$h/1000;
1939
}
1940
 
1941
/**
1942
* return the font decender, this will normally return a negative number
1943
* if you add this number to the baseline, you get the level of the bottom of the font
1944
* it is in the pdf user units
1945
*/
1946
function getFontDecender($size){
1947
  // note that this will most likely return a negative value
1948
  if (!$this->numFonts){
1949
    $this->selectFont('./fonts/Helvetica');
1950
  }
1951
  $h = $this->fonts[$this->currentFont]['FontBBox'][1];
1952
  return $size*$h/1000;
1953
}
1954
 
1955
/**
1956
* filter the text, this is applied to all text just before being inserted into the pdf document
1957
* it escapes the various things that need to be escaped, and so on
1958
*
1959
* @access private
1960
*/
1961
function filterText($text){
1962
  $text = str_replace('\\','\\\\',$text);
1963
  $text = str_replace('(','\(',$text);
1964
  $text = str_replace(')','\)',$text);
1965
  $text = str_replace('&lt;','<',$text);
1966
  $text = str_replace('&gt;','>',$text);
1967
  $text = str_replace('&#039;','\'',$text);
1968
  $text = str_replace('&quot;','"',$text);
1969
  $text = str_replace('&amp;','&',$text);
1970
 
1971
  return $text;
1972
}
1973
 
1974
/**
1975
* given a start position and information about how text is to be laid out, calculate where
1976
* on the page the text will end
1977
*
1978
* @access private
1979
*/
1980
function PRVTgetTextPosition($x,$y,$angle,$size,$wa,$text){
1981
  // given this information return an array containing x and y for the end position as elements 0 and 1
1982
  $w = $this->getTextWidth($size,$text);
1983
  // need to adjust for the number of spaces in this text
1984
  $words = explode(' ',$text);
1985
  $nspaces=count($words)-1;
1986
  $w += $wa*$nspaces;
1987
  $a = deg2rad((float)$angle);
1988
  return array(cos($a)*$w+$x,-sin($a)*$w+$y);
1989
}
1990
 
1991
/**
1992
* wrapper function for PRVTcheckTextDirective1
1993
*
1994
* @access private
1995
*/
1996
function PRVTcheckTextDirective(&$text,$i,&$f){
1997
  $x=0;
1998
  $y=0;
1999
  return $this->PRVTcheckTextDirective1($text,$i,$f,0,$x,$y);
2000
}
2001
 
2002
/**
2003
* checks if the text stream contains a control directive
2004
* if so then makes some changes and returns the number of characters involved in the directive
2005
* this has been re-worked to include everything neccesary to fins the current writing point, so that
2006
* the location can be sent to the callback function if required
2007
* if the directive does not require a font change, then $f should be set to 0
2008
*
2009
* @access private
2010
*/
2011
function PRVTcheckTextDirective1(&$text,$i,&$f,$final,&$x,&$y,$size=0,$angle=0,$wordSpaceAdjust=0){
2012
  $directive = 0;
2013
  $j=$i;
2014
  if ($text[$j]=='<'){
2015
    $j++;
2016
    switch($text[$j]){
2017
      case '/':
2018
        $j++;
2019
        if (strlen($text) <= $j){
2020
          return $directive;
2021
        }
2022
        switch($text[$j]){
2023
          case 'b':
2024
          case 'i':
2025
            $j++;
2026
            if ($text[$j]=='>'){
2027
              $p = strrpos($this->currentTextState,$text[$j-1]);
2028
              if ($p !== false){
2029
                // then there is one to remove
2030
                $this->currentTextState = substr($this->currentTextState,0,$p).substr($this->currentTextState,$p+1);
2031
              }
2032
              $directive=$j-$i+1;
2033
            }
2034
            break;
2035
          case 'c':
2036
            // this this might be a callback function
2037
            $j++;
2038
            $k = strpos($text,'>',$j);
2039
            if ($k!==false && $text[$j]==':'){
2040
              // then this will be treated as a callback directive
2041
              $directive = $k-$i+1;
2042
              $f=0;
2043
              // split the remainder on colons to get the function name and the paramater
2044
              $tmp = substr($text,$j+1,$k-$j-1);
2045
              $b1 = strpos($tmp,':');
2046
              if ($b1!==false){
2047
                $func = substr($tmp,0,$b1);
2048
                $parm = substr($tmp,$b1+1);
2049
              } else {
2050
                $func=$tmp;
2051
                $parm='';
2052
              }
2053
              if (!isset($func) || !strlen(trim($func))){
2054
                $directive=0;
2055
              } else {
2056
                // only call the function if this is the final call
2057
                if ($final){
2058
                  // need to assess the text position, calculate the text width to this point
2059
                  // can use getTextWidth to find the text width I think
2060
                  $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,substr($text,0,$i));
2061
                  $info = array('x'=>$tmp[0],'y'=>$tmp[1],'angle'=>$angle,'status'=>'end','p'=>$parm,'nCallback'=>$this->nCallback);
2062
                  $x=$tmp[0];
2063
                  $y=$tmp[1];
2064
                  $ret = $this->$func($info);
2065
                  if (is_array($ret)){
2066
                    // then the return from the callback function could set the position, to start with, later will do font colour, and font
2067
                    foreach($ret as $rk=>$rv){
2068
                      switch($rk){
2069
                        case 'x':
2070
                        case 'y':
2071
                          $$rk=$rv;
2072
                          break;
2073
                      }
2074
                    }
2075
                  }
2076
                  // also remove from to the stack
2077
                  // for simplicity, just take from the end, fix this another day
2078
                  $this->nCallback--;
2079
                  if ($this->nCallback<0){
2080
                    $this->nCallBack=0;
2081
                  }
2082
                }
2083
              }
2084
            }
2085
            break;
2086
        }
2087
        break;
2088
      case 'b':
2089
      case 'i':
2090
        $j++;
2091
        if ($text[$j]=='>'){
2092
          $this->currentTextState.=$text[$j-1];
2093
          $directive=$j-$i+1;
2094
        }
2095
        break;
2096
      case 'C':
2097
        $noClose=1;
2098
      case 'c':
2099
        // this this might be a callback function
2100
        $j++;
2101
        $k = strpos($text,'>',$j);
2102
        if ($k!==false && $text[$j]==':'){
2103
          // then this will be treated as a callback directive
2104
          $directive = $k-$i+1;
2105
          $f=0;
2106
          // split the remainder on colons to get the function name and the paramater
2107
//          $bits = explode(':',substr($text,$j+1,$k-$j-1));
2108
          $tmp = substr($text,$j+1,$k-$j-1);
2109
          $b1 = strpos($tmp,':');
2110
          if ($b1!==false){
2111
            $func = substr($tmp,0,$b1);
2112
            $parm = substr($tmp,$b1+1);
2113
          } else {
2114
            $func=$tmp;
2115
            $parm='';
2116
          }
2117
          if (!isset($func) || !strlen(trim($func))){
2118
            $directive=0;
2119
          } else {
2120
            // only call the function if this is the final call, ie, the one actually doing printing, not measurement
2121
            if ($final){
2122
              // need to assess the text position, calculate the text width to this point
2123
              // can use getTextWidth to find the text width I think
2124
              // also add the text height and decender
2125
              $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,substr($text,0,$i));
2126
              $info = array('x'=>$tmp[0],'y'=>$tmp[1],'angle'=>$angle,'status'=>'start','p'=>$parm,'f'=>$func,'height'=>$this->getFontHeight($size),'decender'=>$this->getFontDecender($size));
2127
              $x=$tmp[0];
2128
              $y=$tmp[1];
2129
              if (!isset($noClose) || !$noClose){
2130
                // only add to the stack if this is a small 'c', therefore is a start-stop pair
2131
                $this->nCallback++;
2132
                $info['nCallback']=$this->nCallback;
2133
                $this->callback[$this->nCallback]=$info;
2134
              }
2135
              $ret = $this->$func($info);
2136
              if (is_array($ret)){
2137
                // then the return from the callback function could set the position, to start with, later will do font colour, and font
2138
                foreach($ret as $rk=>$rv){
2139
                  switch($rk){
2140
                    case 'x':
2141
                    case 'y':
2142
                      $$rk=$rv;
2143
                      break;
2144
                  }
2145
                }
2146
              }
2147
            }
2148
          }
2149
        }
2150
        break;
2151
    }
2152
  }
2153
  return $directive;
2154
}
2155
 
2156
/**
2157
* add text to the document, at a specified location, size and angle on the page
2158
*/
2159
function addText($x,$y,$size,$text,$angle=0,$wordSpaceAdjust=0){
2160
  if (!$this->numFonts){$this->selectFont('./fonts/Helvetica');}
2161
 
2162
  // if there are any open callbacks, then they should be called, to show the start of the line
2163
  if ($this->nCallback>0){
2164
    for ($i=$this->nCallback;$i>0;$i--){
2165
      // call each function
2166
      $info = array('x'=>$x,'y'=>$y,'angle'=>$angle,'status'=>'sol','p'=>$this->callback[$i]['p'],'nCallback'=>$this->callback[$i]['nCallback'],'height'=>$this->callback[$i]['height'],'decender'=>$this->callback[$i]['decender']);
2167
      $func = $this->callback[$i]['f'];
2168
      $this->$func($info);
2169
    }
2170
  }
2171
  if ($angle==0){
2172
    $this->objects[$this->currentContents]['c'].="\n".'BT '.sprintf('%.3f',$x).' '.sprintf('%.3f',$y).' Td';
2173
  } else {
2174
    $a = deg2rad((float)$angle);
2175
    $tmp = "\n".'BT ';
2176
    $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' ';
2177
    $tmp .= sprintf('%.3f',$x).' '.sprintf('%.3f',$y).' Tm';
2178
    $this->objects[$this->currentContents]['c'] .= $tmp;
2179
  }
2180
  if ($wordSpaceAdjust!=0 || $wordSpaceAdjust != $this->wordSpaceAdjust){
2181
    $this->wordSpaceAdjust=$wordSpaceAdjust;
2182
    $this->objects[$this->currentContents]['c'].=' '.sprintf('%.3f',$wordSpaceAdjust).' Tw';
2183
  }
2184
  $len=strlen($text);
2185
  $start=0;
2186
  for ($i=0;$i<$len;$i++){
2187
    $f=1;
2188
    $directive = $this->PRVTcheckTextDirective($text,$i,$f);
2189
    if ($directive){
2190
      // then we should write what we need to
2191
      if ($i>$start){
2192
        $part = substr($text,$start,$i-$start);
2193
        $this->objects[$this->currentContents]['c'].=' /F'.$this->currentFontNum.' '.sprintf('%.1f',$size).' Tf ';
2194
        $this->objects[$this->currentContents]['c'].=' ('.$this->filterText($part).') Tj';
2195
      }
2196
      if ($f){
2197
        // then there was nothing drastic done here, restore the contents
2198
        $this->setCurrentFont();
2199
      } else {
2200
        $this->objects[$this->currentContents]['c'] .= ' ET';
2201
        $f=1;
2202
        $xp=$x;
2203
        $yp=$y;
2204
        $directive = $this->PRVTcheckTextDirective1($text,$i,$f,1,$xp,$yp,$size,$angle,$wordSpaceAdjust);
2205
 
2206
        // restart the text object
2207
          if ($angle==0){
2208
            $this->objects[$this->currentContents]['c'].="\n".'BT '.sprintf('%.3f',$xp).' '.sprintf('%.3f',$yp).' Td';
2209
          } else {
2210
            $a = deg2rad((float)$angle);
2211
            $tmp = "\n".'BT ';
2212
            $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' ';
2213
            $tmp .= sprintf('%.3f',$xp).' '.sprintf('%.3f',$yp).' Tm';
2214
            $this->objects[$this->currentContents]['c'] .= $tmp;
2215
          }
2216
          if ($wordSpaceAdjust!=0 || $wordSpaceAdjust != $this->wordSpaceAdjust){
2217
            $this->wordSpaceAdjust=$wordSpaceAdjust;
2218
            $this->objects[$this->currentContents]['c'].=' '.sprintf('%.3f',$wordSpaceAdjust).' Tw';
2219
          }
2220
      }
2221
      // and move the writing point to the next piece of text
2222
      $i=$i+$directive-1;
2223
      $start=$i+1;
2224
    }
2225
 
2226
  }
2227
  if ($start<$len){
2228
    $part = substr($text,$start);
2229
    $this->objects[$this->currentContents]['c'].=' /F'.$this->currentFontNum.' '.sprintf('%.1f',$size).' Tf ';
2230
    $this->objects[$this->currentContents]['c'].=' ('.$this->filterText($part).') Tj';
2231
  }
2232
  $this->objects[$this->currentContents]['c'].=' ET';
2233
 
2234
  // if there are any open callbacks, then they should be called, to show the end of the line
2235
  if ($this->nCallback>0){
2236
    for ($i=$this->nCallback;$i>0;$i--){
2237
      // call each function
2238
      $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,$text);
2239
      $info = array('x'=>$tmp[0],'y'=>$tmp[1],'angle'=>$angle,'status'=>'eol','p'=>$this->callback[$i]['p'],'nCallback'=>$this->callback[$i]['nCallback'],'height'=>$this->callback[$i]['height'],'decender'=>$this->callback[$i]['decender']);
2240
      $func = $this->callback[$i]['f'];
2241
      $this->$func($info);
2242
    }
2243
  }
2244
 
2245
}
2246
 
2247
/**
2248
* calculate how wide a given text string will be on a page, at a given size.
2249
* this can be called externally, but is alse used by the other class functions
2250
*/
2251
function getTextWidth($size,$text){
2252
  // this function should not change any of the settings, though it will need to
2253
  // track any directives which change during calculation, so copy them at the start
2254
  // and put them back at the end.
2255
  $store_currentTextState = $this->currentTextState;
2256
 
2257
  if (!$this->numFonts){
2258
    $this->selectFont('./fonts/Helvetica');
2259
  }
2260
 
2261
  // converts a number or a float to a string so it can get the width
2262
  $text = "$text";
2263
 
2264
  // hmm, this is where it all starts to get tricky - use the font information to
2265
  // calculate the width of each character, add them up and convert to user units
2266
  $w=0;
2267
  $len=strlen($text);
2268
  $cf = $this->currentFont;
2269
  for ($i=0;$i<$len;$i++){
2270
    $f=1;
2271
    $directive = $this->PRVTcheckTextDirective($text,$i,$f);
2272
    if ($directive){
2273
      if ($f){
2274
        $this->setCurrentFont();
2275
        $cf = $this->currentFont;
2276
      }
2277
      $i=$i+$directive-1;
2278
    } else {
2279
      $char=ord($text[$i]);
2280
      if (isset($this->fonts[$cf]['differences'][$char])){
2281
        // then this character is being replaced by another
2282
        $name = $this->fonts[$cf]['differences'][$char];
2283
        if (isset($this->fonts[$cf]['C'][$name]['WX'])){
2284
          $w+=$this->fonts[$cf]['C'][$name]['WX'];
2285
        }
2286
      } else if (isset($this->fonts[$cf]['C'][$char]['WX'])){
2287
        $w+=$this->fonts[$cf]['C'][$char]['WX'];
2288
      }
2289
    }
2290
  }
2291
 
2292
  $this->currentTextState = $store_currentTextState;
2293
  $this->setCurrentFont();
2294
 
2295
  return $w*$size/1000;
2296
}
2297
 
2298
/**
2299
* do a part of the calculation for sorting out the justification of the text
2300
*
2301
* @access private
2302
*/
2303
function PRVTadjustWrapText($text,$actual,$width,&$x,&$adjust,$justification){
2304
  switch ($justification){
2305
    case 'left':
2306
      return;
2307
      break;
2308
    case 'right':
2309
      $x+=$width-$actual;
2310
      break;
2311
    case 'center':
2312
    case 'centre':
2313
      $x+=($width-$actual)/2;
2314
      break;
2315
    case 'full':
2316
      // count the number of words
2317
      $words = explode(' ',$text);
2318
      $nspaces=count($words)-1;
2319
      if ($nspaces>0){
2320
        $adjust = ($width-$actual)/$nspaces;
2321
      } else {
2322
        $adjust=0;
2323
      }
2324
      break;
2325
  }
2326
}
2327
 
2328
/**
2329
* add text to the page, but ensure that it fits within a certain width
2330
* if it does not fit then put in as much as possible, splitting at word boundaries
2331
* and return the remainder.
2332
* justification and angle can also be specified for the text
2333
*/
2334
function addTextWrap($x,$y,$width,$size,$text,$justification='left',$angle=0,$test=0){
2335
  // this will display the text, and if it goes beyond the width $width, will backtrack to the
2336
  // previous space or hyphen, and return the remainder of the text.
2337
 
2338
  // $justification can be set to 'left','right','center','centre','full'
2339
 
2340
  // need to store the initial text state, as this will change during the width calculation
2341
  // but will need to be re-set before printing, so that the chars work out right
2342
  $store_currentTextState = $this->currentTextState;
2343
 
2344
  if (!$this->numFonts){$this->selectFont('./fonts/Helvetica');}
2345
  if ($width<=0){
2346
    // error, pretend it printed ok, otherwise risking a loop
2347
    return '';
2348
  }
2349
  $w=0;
2350
  $break=0;
2351
  $breakWidth=0;
2352
  $len=strlen($text);
2353
  $cf = $this->currentFont;
2354
  $tw = $width/$size*1000;
2355
  for ($i=0;$i<$len;$i++){
2356
    $f=1;
2357
    $directive = $this->PRVTcheckTextDirective($text,$i,$f);
2358
    if ($directive){
2359
      if ($f){
2360
        $this->setCurrentFont();
2361
        $cf = $this->currentFont;
2362
      }
2363
      $i=$i+$directive-1;
2364
    } else {
2365
      $cOrd = ord($text[$i]);
2366
      if (isset($this->fonts[$cf]['differences'][$cOrd])){
2367
        // then this character is being replaced by another
2368
        $cOrd2 = $this->fonts[$cf]['differences'][$cOrd];
2369
      } else {
2370
        $cOrd2 = $cOrd;
2371
      }
2372
 
2373
      if (isset($this->fonts[$cf]['C'][$cOrd2]['WX'])){
2374
        $w+=$this->fonts[$cf]['C'][$cOrd2]['WX'];
2375
      }
2376
      if ($w>$tw){
2377
        // then we need to truncate this line
2378
        if ($break>0){
2379
          // then we have somewhere that we can split :)
2380
          if ($text[$break]==' '){
2381
            $tmp = substr($text,0,$break);
2382
          } else {
2383
            $tmp = substr($text,0,$break+1);
2384
          }
2385
          $adjust=0;
2386
          $this->PRVTadjustWrapText($tmp,$breakWidth,$width,$x,$adjust,$justification);
2387
 
2388
          // reset the text state
2389
          $this->currentTextState = $store_currentTextState;
2390
          $this->setCurrentFont();
2391
          if (!$test){
2392
            $this->addText($x,$y,$size,$tmp,$angle,$adjust);
2393
          }
2394
          return substr($text,$break+1);
2395
        } else {
2396
          // just split before the current character
2397
          $tmp = substr($text,0,$i);
2398
          $adjust=0;
2399
          $ctmp=ord($text[$i]);
2400
          if (isset($this->fonts[$cf]['differences'][$ctmp])){
2401
            $ctmp=$this->fonts[$cf]['differences'][$ctmp];
2402
          }
2403
          $tmpw=($w-$this->fonts[$cf]['C'][$ctmp]['WX'])*$size/1000;
2404
          $this->PRVTadjustWrapText($tmp,$tmpw,$width,$x,$adjust,$justification);
2405
          // reset the text state
2406
          $this->currentTextState = $store_currentTextState;
2407
          $this->setCurrentFont();
2408
          if (!$test){
2409
            $this->addText($x,$y,$size,$tmp,$angle,$adjust);
2410
          }
2411
          return substr($text,$i);
2412
        }
2413
      }
2414
      if ($text[$i]=='-'){
2415
        $break=$i;
2416
        $breakWidth = $w*$size/1000;
2417
      }
2418
      if ($text[$i]==' '){
2419
        $break=$i;
2420
        $ctmp=ord($text[$i]);
2421
        if (isset($this->fonts[$cf]['differences'][$ctmp])){
2422
          $ctmp=$this->fonts[$cf]['differences'][$ctmp];
2423
        }
2424
        $breakWidth = ($w-$this->fonts[$cf]['C'][$ctmp]['WX'])*$size/1000;
2425
      }
2426
    }
2427
  }
2428
  // then there was no need to break this line
2429
  if ($justification=='full'){
2430
    $justification='left';
2431
  }
2432
  $adjust=0;
2433
  $tmpw=$w*$size/1000;
2434
  $this->PRVTadjustWrapText($text,$tmpw,$width,$x,$adjust,$justification);
2435
  // reset the text state
2436
  $this->currentTextState = $store_currentTextState;
2437
  $this->setCurrentFont();
2438
  if (!$test){
2439
    $this->addText($x,$y,$size,$text,$angle,$adjust,$angle);
2440
  }
2441
  return '';
2442
}
2443
 
2444
/**
2445
* this will be called at a new page to return the state to what it was on the
2446
* end of the previous page, before the stack was closed down
2447
* This is to get around not being able to have open 'q' across pages
2448
*
2449
*/
2450
function saveState($pageEnd=0){
2451
  if ($pageEnd){
2452
    // this will be called at a new page to return the state to what it was on the
2453
    // end of the previous page, before the stack was closed down
2454
    // This is to get around not being able to have open 'q' across pages
2455
    $opt = $this->stateStack[$pageEnd]; // ok to use this as stack starts numbering at 1
2456
    $this->setColor($opt['col']['r'],$opt['col']['g'],$opt['col']['b'],1);
2457
    $this->setStrokeColor($opt['str']['r'],$opt['str']['g'],$opt['str']['b'],1);
2458
    $this->objects[$this->currentContents]['c'].="\n".$opt['lin'];
2459
//    $this->currentLineStyle = $opt['lin'];
2460
  } else {
2461
    $this->nStateStack++;
2462
    $this->stateStack[$this->nStateStack]=array(
2463
      'col'=>$this->currentColour
2464
     ,'str'=>$this->currentStrokeColour
2465
     ,'lin'=>$this->currentLineStyle
2466
    );
2467
  }
2468
  $this->objects[$this->currentContents]['c'].="\nq";
2469
}
2470
 
2471
/**
2472
* restore a previously saved state
2473
*/
2474
function restoreState($pageEnd=0){
2475
  if (!$pageEnd){
2476
    $n = $this->nStateStack;
2477
    $this->currentColour = $this->stateStack[$n]['col'];
2478
    $this->currentStrokeColour = $this->stateStack[$n]['str'];
2479
    $this->objects[$this->currentContents]['c'].="\n".$this->stateStack[$n]['lin'];
2480
    $this->currentLineStyle = $this->stateStack[$n]['lin'];
2481
    unset($this->stateStack[$n]);
2482
    $this->nStateStack--;
2483
  }
2484
  $this->objects[$this->currentContents]['c'].="\nQ";
2485
}
2486
 
2487
/**
2488
* make a loose object, the output will go into this object, until it is closed, then will revert to
2489
* the current one.
2490
* this object will not appear until it is included within a page.
2491
* the function will return the object number
2492
*/
2493
function openObject(){
2494
  $this->nStack++;
2495
  $this->stack[$this->nStack]=array('c'=>$this->currentContents,'p'=>$this->currentPage);
2496
  // add a new object of the content type, to hold the data flow
2497
  $this->numObj++;
2498
  $this->o_contents($this->numObj,'new');
2499
  $this->currentContents=$this->numObj;
2500
  $this->looseObjects[$this->numObj]=1;
2501
 
2502
  return $this->numObj;
2503
}
2504
 
2505
/**
2506
* open an existing object for editing
2507
*/
2508
function reopenObject($id){
2509
   $this->nStack++;
2510
   $this->stack[$this->nStack]=array('c'=>$this->currentContents,'p'=>$this->currentPage);
2511
   $this->currentContents=$id;
2512
   // also if this object is the primary contents for a page, then set the current page to its parent
2513
   if (isset($this->objects[$id]['onPage'])){
2514
     $this->currentPage = $this->objects[$id]['onPage'];
2515
   }
2516
}
2517
 
2518
/**
2519
* close an object
2520
*/
2521
function closeObject(){
2522
  // close the object, as long as there was one open in the first place, which will be indicated by
2523
  // an objectId on the stack.
2524
  if ($this->nStack>0){
2525
    $this->currentContents=$this->stack[$this->nStack]['c'];
2526
    $this->currentPage=$this->stack[$this->nStack]['p'];
2527
    $this->nStack--;
2528
    // easier to probably not worry about removing the old entries, they will be overwritten
2529
    // if there are new ones.
2530
  }
2531
}
2532
 
2533
/**
2534
* stop an object from appearing on pages from this point on
2535
*/
2536
function stopObject($id){
2537
  // if an object has been appearing on pages up to now, then stop it, this page will
2538
  // be the last one that could contian it.
2539
  if (isset($this->addLooseObjects[$id])){
2540
    $this->addLooseObjects[$id]='';
2541
  }
2542
}
2543
 
2544
/**
2545
* after an object has been created, it wil only show if it has been added, using this function.
2546
*/
2547
function addObject($id,$options='add'){
2548
  // add the specified object to the page
2549
  if (isset($this->looseObjects[$id]) && $this->currentContents!=$id){
2550
    // then it is a valid object, and it is not being added to itself
2551
    switch($options){
2552
      case 'all':
2553
        // then this object is to be added to this page (done in the next block) and
2554
        // all future new pages.
2555
        $this->addLooseObjects[$id]='all';
2556
      case 'add':
2557
        if (isset($this->objects[$this->currentContents]['onPage'])){
2558
          // then the destination contents is the primary for the page
2559
          // (though this object is actually added to that page)
2560
          $this->o_page($this->objects[$this->currentContents]['onPage'],'content',$id);
2561
        }
2562
        break;
2563
      case 'even':
2564
        $this->addLooseObjects[$id]='even';
2565
        $pageObjectId=$this->objects[$this->currentContents]['onPage'];
2566
        if ($this->objects[$pageObjectId]['info']['pageNum']%2==0){
2567
          $this->addObject($id); // hacky huh :)
2568
        }
2569
        break;
2570
      case 'odd':
2571
        $this->addLooseObjects[$id]='odd';
2572
        $pageObjectId=$this->objects[$this->currentContents]['onPage'];
2573
        if ($this->objects[$pageObjectId]['info']['pageNum']%2==1){
2574
          $this->addObject($id); // hacky huh :)
2575
        }
2576
        break;
2577
      case 'next':
2578
        $this->addLooseObjects[$id]='all';
2579
        break;
2580
      case 'nexteven':
2581
        $this->addLooseObjects[$id]='even';
2582
        break;
2583
      case 'nextodd':
2584
        $this->addLooseObjects[$id]='odd';
2585
        break;
2586
    }
2587
  }
2588
}
2589
 
2590
/**
2591
* add content to the documents info object
2592
*/
2593
function addInfo($label,$value=0){
2594
  // this will only work if the label is one of the valid ones.
2595
  // modify this so that arrays can be passed as well.
2596
  // if $label is an array then assume that it is key=>value pairs
2597
  // else assume that they are both scalar, anything else will probably error
2598
  if (is_array($label)){
2599
    foreach ($label as $l=>$v){
2600
      $this->o_info($this->infoObject,$l,$v);
2601
    }
2602
  } else {
2603
    $this->o_info($this->infoObject,$label,$value);
2604
  }
2605
}
2606
 
2607
/**
2608
* set the viewer preferences of the document, it is up to the browser to obey these.
2609
*/
2610
function setPreferences($label,$value=0){
2611
  // this will only work if the label is one of the valid ones.
2612
  if (is_array($label)){
2613
    foreach ($label as $l=>$v){
2614
      $this->o_catalog($this->catalogId,'viewerPreferences',array($l=>$v));
2615
    }
2616
  } else {
2617
    $this->o_catalog($this->catalogId,'viewerPreferences',array($label=>$value));
2618
  }
2619
}
2620
 
2621
/**
2622
* extract an integer from a position in a byte stream
2623
*
2624
* @access private
2625
*/
2626
function PRVT_getBytes(&$data,$pos,$num){
2627
  // return the integer represented by $num bytes from $pos within $data
2628
  $ret=0;
2629
  for ($i=0;$i<$num;$i++){
2630
    $ret=$ret*256;
2631
    $ret+=ord($data[$pos+$i]);
2632
  }
2633
  return $ret;
2634
}
2635
 
2636
/**
2637
* add a PNG image into the document, from a file
2638
* this should work with remote files
2639
*/
2640
function addPngFromFile($file,$x,$y,$w=0,$h=0){
2641
  // read in a png file, interpret it, then add to the system
2642
  $error=0;
2643
  $tmp = get_magic_quotes_runtime();
2644
  set_magic_quotes_runtime(0);
2645
  $fp = @fopen($file,'rb');
2646
  if ($fp){
2647
    $data='';
2648
    while(!feof($fp)){
2649
      $data .= fread($fp,1024);
2650
    }
2651
    fclose($fp);
2652
  } else {
2653
    $error = 1;
2654
    $errormsg = 'trouble opening file: '.$file;
2655
  }
2656
  set_magic_quotes_runtime($tmp);
2657
 
2658
  if (!$error){
2659
    $header = chr(137).chr(80).chr(78).chr(71).chr(13).chr(10).chr(26).chr(10);
2660
    if (substr($data,0,8)!=$header){
2661
      $error=1;
2662
      $errormsg = 'this file does not have a valid header';
2663
    }
2664
  }
2665
 
2666
  if (!$error){
2667
    // set pointer
2668
    $p = 8;
2669
    $len = strlen($data);
2670
    // cycle through the file, identifying chunks
2671
    $haveHeader=0;
2672
    $info=array();
2673
    $idata='';
2674
    $pdata='';
2675
    while ($p<$len){
2676
      $chunkLen = $this->PRVT_getBytes($data,$p,4);
2677
      $chunkType = substr($data,$p+4,4);
2678
//      echo $chunkType.' - '.$chunkLen.'<br>';
2679
 
2680
      switch($chunkType){
2681
        case 'IHDR':
2682
          // this is where all the file information comes from
2683
          $info['width']=$this->PRVT_getBytes($data,$p+8,4);
2684
          $info['height']=$this->PRVT_getBytes($data,$p+12,4);
2685
          $info['bitDepth']=ord($data[$p+16]);
2686
          $info['colorType']=ord($data[$p+17]);
2687
          $info['compressionMethod']=ord($data[$p+18]);
2688
          $info['filterMethod']=ord($data[$p+19]);
2689
          $info['interlaceMethod']=ord($data[$p+20]);
2690
//print_r($info);
2691
          $haveHeader=1;
2692
          if ($info['compressionMethod']!=0){
2693
            $error=1;
2694
            $errormsg = 'unsupported compression method';
2695
          }
2696
          if ($info['filterMethod']!=0){
2697
            $error=1;
2698
            $errormsg = 'unsupported filter method';
2699
          }
2700
          break;
2701
        case 'PLTE':
2702
          $pdata.=substr($data,$p+8,$chunkLen);
2703
          break;
2704
        case 'IDAT':
2705
          $idata.=substr($data,$p+8,$chunkLen);
2706
          break;
2707
        case 'tRNS':
2708
          //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk
2709
          //print "tRNS found, color type = ".$info['colorType']."<BR>";
2710
          $transparency = array();
2711
          if ($info['colorType'] == 3) { // indexed color, rbg
2712
          /* corresponding to entries in the plte chunk
2713
          Alpha for palette index 0: 1 byte
2714
          Alpha for palette index 1: 1 byte
2715
          ...etc...
2716
          */
2717
            // there will be one entry for each palette entry. up until the last non-opaque entry.
2718
            // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent)
2719
            $transparency['type']='indexed';
2720
            $numPalette = strlen($pdata)/3;
2721
            $trans=0;
2722
            for ($i=$chunkLen;$i>=0;$i--){
2723
              if (ord($data[$p+8+$i])==0){
2724
                $trans=$i;
2725
              }
2726
            }
2727
            $transparency['data'] = $trans;
2728
 
2729
          } elseif($info['colorType'] == 0) { // grayscale
2730
          /* corresponding to entries in the plte chunk
2731
          Gray: 2 bytes, range 0 .. (2^bitdepth)-1
2732
          */
2733
//            $transparency['grayscale']=$this->PRVT_getBytes($data,$p+8,2); // g = grayscale
2734
            $transparency['type']='indexed';
2735
            $transparency['data'] = ord($data[$p+8+1]);
2736
 
2737
          } elseif($info['colorType'] == 2) { // truecolor
2738
          /* corresponding to entries in the plte chunk
2739
          Red: 2 bytes, range 0 .. (2^bitdepth)-1
2740
          Green: 2 bytes, range 0 .. (2^bitdepth)-1
2741
          Blue: 2 bytes, range 0 .. (2^bitdepth)-1
2742
          */
2743
            $transparency['r']=$this->PRVT_getBytes($data,$p+8,2); // r from truecolor
2744
            $transparency['g']=$this->PRVT_getBytes($data,$p+10,2); // g from truecolor
2745
            $transparency['b']=$this->PRVT_getBytes($data,$p+12,2); // b from truecolor
2746
 
2747
          } else {
2748
          //unsupported transparency type
2749
          }
2750
          // KS End new code
2751
          break;
2752
        default:
2753
          break;
2754
      }
2755
 
2756
      $p += $chunkLen+12;
2757
    }
2758
 
2759
    if(!$haveHeader){
2760
      $error = 1;
2761
      $errormsg = 'information header is missing';
2762
    }
2763
    if (isset($info['interlaceMethod']) && $info['interlaceMethod']){
2764
      $error = 1;
2765
      $errormsg = 'There appears to be no support for interlaced images in pdf.';
2766
    }
2767
  }
2768
 
2769
  if (!$error && $info['bitDepth'] > 8){
2770
    $error = 1;
2771
    $errormsg = 'only bit depth of 8 or less is supported';
2772
  }
2773
 
2774
  if (!$error){
2775
    if ($info['colorType']!=2 && $info['colorType']!=0 && $info['colorType']!=3){
2776
      $error = 1;
2777
      $errormsg = 'transparancey alpha channel not supported, transparency only supported for palette images.';
2778
    } else {
2779
      switch ($info['colorType']){
2780
        case 3:
2781
          $color = 'DeviceRGB';
2782
          $ncolor=1;
2783
          break;
2784
        case 2:
2785
          $color = 'DeviceRGB';
2786
          $ncolor=3;
2787
          break;
2788
        case 0:
2789
          $color = 'DeviceGray';
2790
          $ncolor=1;
2791
          break;
2792
      }
2793
    }
2794
  }
2795
  if ($error){
2796
    $this->addMessage('PNG error - ('.$file.') '.$errormsg);
2797
    return;
2798
  }
2799
  if ($w==0){
2800
    $w=$h/$info['height']*$info['width'];
2801
  }
2802
  if ($h==0){
2803
    $h=$w*$info['height']/$info['width'];
2804
  }
2805
//print_r($info);
2806
  // so this image is ok... add it in.
2807
  $this->numImages++;
2808
  $im=$this->numImages;
2809
  $label='I'.$im;
2810
  $this->numObj++;
2811
//  $this->o_image($this->numObj,'new',array('label'=>$label,'data'=>$idata,'iw'=>$w,'ih'=>$h,'type'=>'png','ic'=>$info['width']));
2812
  $options = array('label'=>$label,'data'=>$idata,'bitsPerComponent'=>$info['bitDepth'],'pdata'=>$pdata
2813
                                      ,'iw'=>$info['width'],'ih'=>$info['height'],'type'=>'png','color'=>$color,'ncolor'=>$ncolor);
2814
  if (isset($transparency)){
2815
    $options['transparency']=$transparency;
2816
  }
2817
  $this->o_image($this->numObj,'new',$options);
2818
 
2819
  $this->objects[$this->currentContents]['c'].="\nq";
2820
  $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$w)." 0 0 ".sprintf('%.3f',$h)." ".sprintf('%.3f',$x)." ".sprintf('%.3f',$y)." cm";
2821
  $this->objects[$this->currentContents]['c'].="\n/".$label.' Do';
2822
  $this->objects[$this->currentContents]['c'].="\nQ";
2823
}
2824
 
2825
/**
2826
* add a JPEG image into the document, from a file
2827
*/
2828
function addJpegFromFile($img,$x,$y,$w=0,$h=0){
2829
  // attempt to add a jpeg image straight from a file, using no GD commands
2830
  // note that this function is unable to operate on a remote file.
2831
 
2832
  if (!file_exists($img)){
2833
    return;
2834
  }
2835
 
2836
  $tmp=getimagesize($img);
2837
  $imageWidth=$tmp[0];
2838
  $imageHeight=$tmp[1];
2839
 
2840
  if (isset($tmp['channels'])){
2841
    $channels = $tmp['channels'];
2842
  } else {
2843
    $channels = 3;
2844
  }
2845
 
2846
  if ($w<=0 && $h<=0){
2847
    $w=$imageWidth;
2848
  }
2849
  if ($w==0){
2850
    $w=$h/$imageHeight*$imageWidth;
2851
  }
2852
  if ($h==0){
2853
    $h=$w*$imageHeight/$imageWidth;
2854
  }
2855
 
2856
  $fp=fopen($img,'rb');
2857
 
2858
  $tmp = get_magic_quotes_runtime();
2859
  set_magic_quotes_runtime(0);
2860
  $data = fread($fp,filesize($img));
2861
  set_magic_quotes_runtime($tmp);
2862
 
2863
  fclose($fp);
2864
 
2865
  $this->addJpegImage_common($data,$x,$y,$w,$h,$imageWidth,$imageHeight,$channels);
2866
}
2867
 
2868
/**
2869
* add an image into the document, from a GD object
2870
* this function is not all that reliable, and I would probably encourage people to use
2871
* the file based functions
2872
*/
2873
function addImage(&$img,$x,$y,$w=0,$h=0,$quality=75){
2874
  // add a new image into the current location, as an external object
2875
  // add the image at $x,$y, and with width and height as defined by $w & $h
2876
 
2877
  // note that this will only work with full colour images and makes them jpg images for display
2878
  // later versions could present lossless image formats if there is interest.
2879
 
2880
  // there seems to be some problem here in that images that have quality set above 75 do not appear
2881
  // not too sure why this is, but in the meantime I have restricted this to 75.
2882
  if ($quality>75){
2883
    $quality=75;
2884
  }
2885
 
2886
  // if the width or height are set to zero, then set the other one based on keeping the image
2887
  // height/width ratio the same, if they are both zero, then give up :)
2888
  $imageWidth=imagesx($img);
2889
  $imageHeight=imagesy($img);
2890
 
2891
  if ($w<=0 && $h<=0){
2892
    return;
2893
  }
2894
  if ($w==0){
2895
    $w=$h/$imageHeight*$imageWidth;
2896
  }
2897
  if ($h==0){
2898
    $h=$w*$imageHeight/$imageWidth;
2899
  }
2900
 
2901
  // gotta get the data out of the img..
2902
 
2903
  // so I write to a temp file, and then read it back.. soo ugly, my apologies.
2904
  $tmpDir='/tmp';
2905
  $tmpName=tempnam($tmpDir,'img');
2906
  imagejpeg($img,$tmpName,$quality);
2907
  $fp=fopen($tmpName,'rb');
2908
 
2909
  $tmp = get_magic_quotes_runtime();
2910
  set_magic_quotes_runtime(0);
2911
  $fp = @fopen($tmpName,'rb');
2912
  if ($fp){
2913
    $data='';
2914
    while(!feof($fp)){
2915
      $data .= fread($fp,1024);
2916
    }
2917
    fclose($fp);
2918
  } else {
2919
    $error = 1;
2920
    $errormsg = 'trouble opening file';
2921
  }
2922
//  $data = fread($fp,filesize($tmpName));
2923
  set_magic_quotes_runtime($tmp);
2924
//  fclose($fp);
2925
  unlink($tmpName);
2926
  $this->addJpegImage_common($data,$x,$y,$w,$h,$imageWidth,$imageHeight);
2927
}
2928
 
2929
/**
2930
* common code used by the two JPEG adding functions
2931
*
2932
* @access private
2933
*/
2934
function addJpegImage_common(&$data,$x,$y,$w=0,$h=0,$imageWidth,$imageHeight,$channels=3){
2935
  // note that this function is not to be called externally
2936
  // it is just the common code between the GD and the file options
2937
  $this->numImages++;
2938
  $im=$this->numImages;
2939
  $label='I'.$im;
2940
  $this->numObj++;
2941
  $this->o_image($this->numObj,'new',array('label'=>$label,'data'=>$data,'iw'=>$imageWidth,'ih'=>$imageHeight,'channels'=>$channels));
2942
 
2943
  $this->objects[$this->currentContents]['c'].="\nq";
2944
  $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$w)." 0 0 ".sprintf('%.3f',$h)." ".sprintf('%.3f',$x)." ".sprintf('%.3f',$y)." cm";
2945
  $this->objects[$this->currentContents]['c'].="\n/".$label.' Do';
2946
  $this->objects[$this->currentContents]['c'].="\nQ";
2947
}
2948
 
2949
/**
2950
* specify where the document should open when it first starts
2951
*/
2952
function openHere($style,$a=0,$b=0,$c=0){
2953
  // this function will open the document at a specified page, in a specified style
2954
  // the values for style, and the required paramters are:
2955
  // 'XYZ'  left, top, zoom
2956
  // 'Fit'
2957
  // 'FitH' top
2958
  // 'FitV' left
2959
  // 'FitR' left,bottom,right
2960
  // 'FitB'
2961
  // 'FitBH' top
2962
  // 'FitBV' left
2963
  $this->numObj++;
2964
  $this->o_destination($this->numObj,'new',array('page'=>$this->currentPage,'type'=>$style,'p1'=>$a,'p2'=>$b,'p3'=>$c));
2965
  $id = $this->catalogId;
2966
  $this->o_catalog($id,'openHere',$this->numObj);
2967
}
2968
 
2969
/**
2970
* create a labelled destination within the document
2971
*/
2972
function addDestination($label,$style,$a=0,$b=0,$c=0){
2973
  // associates the given label with the destination, it is done this way so that a destination can be specified after
2974
  // it has been linked to
2975
  // styles are the same as the 'openHere' function
2976
  $this->numObj++;
2977
  $this->o_destination($this->numObj,'new',array('page'=>$this->currentPage,'type'=>$style,'p1'=>$a,'p2'=>$b,'p3'=>$c));
2978
  $id = $this->numObj;
2979
  // store the label->idf relationship, note that this means that labels can be used only once
2980
  $this->destinations["$label"]=$id;
2981
}
2982
 
2983
/**
2984
* define font families, this is used to initialize the font families for the default fonts
2985
* and for the user to add new ones for their fonts. The default bahavious can be overridden should
2986
* that be desired.
2987
*/
2988
function setFontFamily($family,$options=''){
2989
  if (!is_array($options)){
2990
    if ($family=='init'){
2991
      // set the known family groups
2992
      // these font families will be used to enable bold and italic markers to be included
2993
      // within text streams. html forms will be used... <b></b> <i></i>
2994
      $this->fontFamilies['Helvetica.afm']=array(
2995
         'b'=>'Helvetica-Bold.afm'
2996
        ,'i'=>'Helvetica-Oblique.afm'
2997
        ,'bi'=>'Helvetica-BoldOblique.afm'
2998
        ,'ib'=>'Helvetica-BoldOblique.afm'
2999
      );
3000
      $this->fontFamilies['Courier.afm']=array(
3001
         'b'=>'Courier-Bold.afm'
3002
        ,'i'=>'Courier-Oblique.afm'
3003
        ,'bi'=>'Courier-BoldOblique.afm'
3004
        ,'ib'=>'Courier-BoldOblique.afm'
3005
      );
3006
      $this->fontFamilies['Times-Roman.afm']=array(
3007
         'b'=>'Times-Bold.afm'
3008
        ,'i'=>'Times-Italic.afm'
3009
        ,'bi'=>'Times-BoldItalic.afm'
3010
        ,'ib'=>'Times-BoldItalic.afm'
3011
      );
3012
    }
3013
  } else {
3014
    // the user is trying to set a font family
3015
    // note that this can also be used to set the base ones to something else
3016
    if (strlen($family)){
3017
      $this->fontFamilies[$family] = $options;
3018
    }
3019
  }
3020
}
3021
 
3022
/**
3023
* used to add messages for use in debugging
3024
*/
3025
function addMessage($message){
3026
  $this->messages.=$message."\n";
3027
}
3028
 
3029
/**
3030
* a few functions which should allow the document to be treated transactionally.
3031
*/
3032
function transaction($action){
3033
  switch ($action){
3034
    case 'start':
3035
      // store all the data away into the checkpoint variable
3036
      $data = get_object_vars($this);
3037
      $this->checkpoint = $data;
3038
      unset($data);
3039
      break;
3040
    case 'commit':
3041
      if (is_array($this->checkpoint) && isset($this->checkpoint['checkpoint'])){
3042
        $tmp = $this->checkpoint['checkpoint'];
3043
        $this->checkpoint = $tmp;
3044
        unset($tmp);
3045
      } else {
3046
        $this->checkpoint='';
3047
      }
3048
      break;
3049
    case 'rewind':
3050
      // do not destroy the current checkpoint, but move us back to the state then, so that we can try again
3051
      if (is_array($this->checkpoint)){
3052
        // can only abort if were inside a checkpoint
3053
        $tmp = $this->checkpoint;
3054
        foreach ($tmp as $k=>$v){
3055
          if ($k != 'checkpoint'){
3056
            $this->$k=$v;
3057
          }
3058
        }
3059
        unset($tmp);
3060
      }
3061
      break;
3062
    case 'abort':
3063
      if (is_array($this->checkpoint)){
3064
        // can only abort if were inside a checkpoint
3065
        $tmp = $this->checkpoint;
3066
        foreach ($tmp as $k=>$v){
3067
          $this->$k=$v;
3068
        }
3069
        unset($tmp);
3070
      }
3071
      break;
3072
  }
3073
 
3074
}
3075
 
3076
} // end of class
3077
 
3078
?>