combine_pdf 0.2.5 → 0.2.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +273 -27
- data/LICENSE.txt +2 -1
- data/README.md +69 -4
- data/lib/combine_pdf/api.rb +156 -153
- data/lib/combine_pdf/basic_writer.rb +41 -53
- data/lib/combine_pdf/decrypt.rb +238 -228
- data/lib/combine_pdf/exceptions.rb +4 -0
- data/lib/combine_pdf/filter.rb +79 -85
- data/lib/combine_pdf/fonts.rb +451 -462
- data/lib/combine_pdf/page_methods.rb +891 -946
- data/lib/combine_pdf/parser.rb +663 -531
- data/lib/combine_pdf/pdf_protected.rb +341 -126
- data/lib/combine_pdf/pdf_public.rb +492 -454
- data/lib/combine_pdf/renderer.rb +146 -141
- data/lib/combine_pdf/version.rb +1 -2
- data/lib/combine_pdf.rb +14 -18
- data/test/automated +132 -0
- data/test/console +4 -4
- data/test/named_dest +84 -0
- metadata +8 -5
- data/lib/combine_pdf/operations.rb +0 -416
@@ -5,951 +5,896 @@
|
|
5
5
|
## is subject to the same license.
|
6
6
|
########################################################
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
8
|
module CombinePDF
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
#
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
#
|
841
|
-
#
|
842
|
-
#
|
843
|
-
#
|
844
|
-
#
|
845
|
-
#
|
846
|
-
#
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
#
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
#
|
856
|
-
#
|
857
|
-
#
|
858
|
-
#
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
#
|
872
|
-
#
|
873
|
-
#
|
874
|
-
#
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
#
|
882
|
-
#
|
883
|
-
#
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
#
|
889
|
-
|
890
|
-
#
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
#
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
# resources = new_page[:Resources]
|
904
|
-
# if resources[:is_reference_only]
|
905
|
-
# resources = resources[:referenced_object]
|
906
|
-
# raise "Couldn't tap into resources dictionary, as it is a reference and isn't linked." unless resources
|
907
|
-
# end
|
908
|
-
|
909
|
-
# # 2. establich direct access to dictionaries and remove reference values
|
910
|
-
# flatten_resources_dictionaries resources
|
911
|
-
|
912
|
-
# # 3. travel every dictionary to pick up names (keys), change them and add them to the dictionary
|
913
|
-
# resources.each do |k,v|
|
914
|
-
# if v.is_a?(Hash)
|
915
|
-
# new_dictionary = {}
|
916
|
-
# new_name = "Combine" + SecureRandom.hex(7) + "PDF"
|
917
|
-
# i = 1
|
918
|
-
# v.each do |old_key, value|
|
919
|
-
# new_key = (new_name + i.to_s).to_sym
|
920
|
-
# names_dictionary[old_key] = new_key
|
921
|
-
# new_dictionary[new_key] = value
|
922
|
-
# i += 1
|
923
|
-
# end
|
924
|
-
# resources[k] = new_dictionary
|
925
|
-
# end
|
926
|
-
# end
|
927
|
-
|
928
|
-
# # now that we have replaced the names in the resources dictionaries,
|
929
|
-
# # it is time to replace the names inside the stream
|
930
|
-
# # we will need to make sure we have access to the stream injected
|
931
|
-
# # we will user PDFFilter.inflate_object
|
932
|
-
# (new_page[:Contents].is_a?(Array) ? new_page[:Contents] : [new_page[:Contents] ]).each do |c|
|
933
|
-
# stream = c[:referenced_object]
|
934
|
-
# PDFFilter.inflate_object stream
|
935
|
-
# names_dictionary.each do |old_key, new_key|
|
936
|
-
# stream[:raw_stream_content].gsub! _object_to_pdf(old_key), _object_to_pdf(new_key) ##### PRAY(!) that the parsed datawill be correctly reproduced!
|
937
|
-
# end
|
938
|
-
# # patch back to PDF defaults, for OCRed PDF files.
|
939
|
-
# # stream[:raw_stream_content] = "q\nq\nq\nDeviceRGB CS\nDeviceRGB cs\n0 0 0 rg\n0 0 0 RG\n0 Tr\n%s\nQ\nQ\nQ\n" % stream[:raw_stream_content]
|
940
|
-
# # the following was removed for Acrobat Reader compatability: DeviceRGB CS\nDeviceRGB cs\n
|
941
|
-
# stream[:raw_stream_content] = "q\nq\nq\n0 0 0 rg\n0 0 0 RG\n0 Tr\n1 0 0 1 0 0 cm\n%s\nQ\nQ\nQ\n" % stream[:raw_stream_content]
|
942
|
-
# end
|
943
|
-
|
944
|
-
# new_page
|
945
|
-
# end
|
946
|
-
|
947
|
-
|
948
|
-
end
|
949
|
-
|
9
|
+
# This module injects page editing methods into existing page objects and the PDFWriter objects.
|
10
|
+
module Page_Methods
|
11
|
+
include Renderer
|
12
|
+
|
13
|
+
# holds the string that starts a PDF graphic state container - used for wrapping malformed PDF content streams.
|
14
|
+
CONTENT_CONTAINER_START = 'q'.freeze
|
15
|
+
# holds the string that ends a PDF graphic state container - used for wrapping malformed PDF content streams.
|
16
|
+
CONTENT_CONTAINER_MIDDLE = "Q\nq".freeze
|
17
|
+
# holds the string that ends a PDF graphic state container - used for wrapping malformed PDF content streams.
|
18
|
+
CONTENT_CONTAINER_END = 'Q'.freeze
|
19
|
+
|
20
|
+
# accessor (getter) for the secure_injection setting
|
21
|
+
def secure_injection
|
22
|
+
warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
|
23
|
+
@secure_injection
|
24
|
+
end
|
25
|
+
|
26
|
+
# accessor (setter) for the secure_injection setting
|
27
|
+
def secure_injection=(safe)
|
28
|
+
warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
|
29
|
+
@secure_injection = safe
|
30
|
+
end
|
31
|
+
|
32
|
+
# sets secure_injection to `true` and returns self, allowing for chaining methods
|
33
|
+
def make_secure
|
34
|
+
warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
|
35
|
+
@secure_injection = true
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
# sets secure_injection to `false` and returns self, allowing for chaining methods
|
40
|
+
def make_unsecure
|
41
|
+
warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
|
42
|
+
@secure_injection = false
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# the injection method
|
47
|
+
def <<(obj)
|
48
|
+
inject_page obj, true
|
49
|
+
end
|
50
|
+
|
51
|
+
def >>(obj)
|
52
|
+
inject_page obj, false
|
53
|
+
end
|
54
|
+
|
55
|
+
def inject_page(obj, top = true)
|
56
|
+
raise TypeError, "couldn't inject data, expecting a PDF page (Hash type)" unless obj.is_a?(Page_Methods)
|
57
|
+
|
58
|
+
obj = obj.copy(should_secure?(obj)) # obj.copy(secure_injection)
|
59
|
+
|
60
|
+
# following the reference chain and assigning a pointer to the correct Resouces object.
|
61
|
+
# (assignments of Strings, Arrays and Hashes are pointers in Ruby, unless the .dup method is called)
|
62
|
+
|
63
|
+
# setup references to avoid method calls.
|
64
|
+
local_res = resources
|
65
|
+
local_val = nil
|
66
|
+
# setup references to avoid method calls.
|
67
|
+
remote_res = obj.resources
|
68
|
+
remote_val = nil
|
69
|
+
|
70
|
+
# add each of the new resources in the uncoming Page to the local resource Hash
|
71
|
+
obj.resources.each do |key, new_val|
|
72
|
+
# keep CombinePDF structural data intact.
|
73
|
+
next if PDF::PRIVATE_HASH_KEYS.include?(key)
|
74
|
+
# review
|
75
|
+
if local_res[key].nil?
|
76
|
+
# no local data, adopt data from incoming page
|
77
|
+
local_res[key] = new_val
|
78
|
+
# go back to looping, no need to parse the rest of the Ruby
|
79
|
+
next
|
80
|
+
elsif (local_val = actual_object(local_res[key])).is_a?(Hash) && (new_val = actual_object(new_val)).is_a?(Hash)
|
81
|
+
# marge data with priority to the incoming page's data
|
82
|
+
new_val.update local_val # make sure the old values are respected
|
83
|
+
local_val.update new_val # transfer old and new values to the injected page
|
84
|
+
end # Do nothing if array or anything else
|
85
|
+
end
|
86
|
+
|
87
|
+
# concat the Annots array? (what good are named links if the names are in the unaccessible Page Catalog?)
|
88
|
+
# if obj[:Annots]
|
89
|
+
# if (local_val = actual_object(self[:Annots])).nil?
|
90
|
+
# self[:Annots] = obj[:Annots]
|
91
|
+
# elsif local_val.is_a?(Array) && (remote_val = actual_object(obj[:Annots])).is_a?(Array)
|
92
|
+
# local_val.concat remote_val
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
|
96
|
+
# set ProcSet to recommended value
|
97
|
+
resources[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI] # this was recommended by the ISO. 32000-1:2008
|
98
|
+
|
99
|
+
if top # if this is a stamp (overlay)
|
100
|
+
insert_content CONTENT_CONTAINER_START, 0
|
101
|
+
insert_content CONTENT_CONTAINER_MIDDLE
|
102
|
+
self[:Contents].concat obj[:Contents]
|
103
|
+
insert_content CONTENT_CONTAINER_END
|
104
|
+
else # if this was a watermark (underlay? would be lost if the page was scanned, as white might not be transparent)
|
105
|
+
insert_content CONTENT_CONTAINER_MIDDLE, 0
|
106
|
+
insert_content CONTENT_CONTAINER_START, 0
|
107
|
+
self[:Contents].insert 1, *obj[:Contents]
|
108
|
+
insert_content CONTENT_CONTAINER_END
|
109
|
+
end
|
110
|
+
init_contents
|
111
|
+
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# accessor (setter) for the :MediaBox element of the page
|
116
|
+
# dimensions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
|
117
|
+
def mediabox=(dimensions = [0.0, 0.0, 612.0, 792.0])
|
118
|
+
self[:MediaBox] = dimensions
|
119
|
+
end
|
120
|
+
|
121
|
+
# accessor (getter) for the :MediaBox element of the page
|
122
|
+
def mediabox
|
123
|
+
actual_object self[:MediaBox]
|
124
|
+
end
|
125
|
+
|
126
|
+
# accessor (setter) for the :CropBox element of the page
|
127
|
+
# dimensions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
|
128
|
+
def cropbox=(dimensions = [0.0, 0.0, 612.0, 792.0])
|
129
|
+
self[:CropBox] = dimensions
|
130
|
+
end
|
131
|
+
|
132
|
+
# accessor (getter) for the :CropBox element of the page
|
133
|
+
def cropbox
|
134
|
+
actual_object self[:CropBox]
|
135
|
+
end
|
136
|
+
|
137
|
+
# get page size
|
138
|
+
def page_size
|
139
|
+
cropbox || mediabox
|
140
|
+
end
|
141
|
+
|
142
|
+
# accessor (getter) for the :Resources element of the page
|
143
|
+
def resources
|
144
|
+
self[:Resources] ||= {}
|
145
|
+
self[:Resources][:referenced_object] || self[:Resources]
|
146
|
+
end
|
147
|
+
|
148
|
+
# This method adds a simple text box to the Page represented by the PDFWriter class.
|
149
|
+
# This function takes two values:
|
150
|
+
# text:: the text to potin the box.
|
151
|
+
# properties:: a Hash of box properties.
|
152
|
+
# the symbols and values in the properties Hash could be any or all of the following:
|
153
|
+
# x:: the left position of the box.
|
154
|
+
# y:: the BUTTOM position of the box.
|
155
|
+
# width:: the width/length of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
|
156
|
+
# height:: the height of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
|
157
|
+
# text_align:: symbol for horizontal text alignment, can be ":center" (default), ":right", ":left"
|
158
|
+
# text_valign:: symbol for vertical text alignment, can be ":center" (default), ":top", ":buttom"
|
159
|
+
# text_padding:: a Float between 0 and 1, setting the padding for the text. defaults to 0.05 (5%).
|
160
|
+
# font:: a registered font name or an Array of names. defaults to ":Helvetica". The 14 standard fonts names are:
|
161
|
+
# - :"Times-Roman"
|
162
|
+
# - :"Times-Bold"
|
163
|
+
# - :"Times-Italic"
|
164
|
+
# - :"Times-BoldItalic"
|
165
|
+
# - :Helvetica
|
166
|
+
# - :"Helvetica-Bold"
|
167
|
+
# - :"Helvetica-BoldOblique"
|
168
|
+
# - :"Helvetica- Oblique"
|
169
|
+
# - :Courier
|
170
|
+
# - :"Courier-Bold"
|
171
|
+
# - :"Courier-Oblique"
|
172
|
+
# - :"Courier-BoldOblique"
|
173
|
+
# - :Symbol
|
174
|
+
# - :ZapfDingbats
|
175
|
+
# font_size:: an Integer for the font size, or :fit_text to fit the text in the box. defaults to ":fit_text"
|
176
|
+
# max_font_size:: if font_size is set to :fit_text, this will be the maximum font size. defaults to nil (no maximum)
|
177
|
+
# font_color:: text color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to black.
|
178
|
+
# stroke_color:: text stroke color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defounlts to nil (no stroke).
|
179
|
+
# stroke_width:: text stroke width in PDF units. defaults to 0 (none).
|
180
|
+
# box_color:: box fill color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to nil (none).
|
181
|
+
# border_color:: box border color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to nil (none).
|
182
|
+
# border_width:: border width in PDF units. defaults to nil (none).
|
183
|
+
# box_radius:: border radius in PDF units. defaults to 0 (no corner rounding).
|
184
|
+
# opacity:: textbox opacity, a float between 0 (transparent) and 1 (opaque)
|
185
|
+
# ctm:: A PDF complient CTM data array that will manipulate the axis and allow transformations. i.e. `[1,0,0,1,0,0]`
|
186
|
+
def textbox(text, properties = {})
|
187
|
+
options = {
|
188
|
+
x: page_size[0],
|
189
|
+
y: page_size[1],
|
190
|
+
width: 0,
|
191
|
+
height: -1,
|
192
|
+
text_align: :center,
|
193
|
+
text_valign: :center,
|
194
|
+
text_padding: 0.1,
|
195
|
+
font: nil,
|
196
|
+
font_size: :fit_text,
|
197
|
+
max_font_size: nil,
|
198
|
+
font_color: [0, 0, 0],
|
199
|
+
stroke_color: nil,
|
200
|
+
stroke_width: 0,
|
201
|
+
box_color: nil,
|
202
|
+
border_color: nil,
|
203
|
+
border_width: 0,
|
204
|
+
box_radius: 0,
|
205
|
+
opacity: 1,
|
206
|
+
ctm: nil # ~= [1,0,0,1,0,0]
|
207
|
+
}
|
208
|
+
options.update properties
|
209
|
+
# reset the length and height to meaningful values, if negative
|
210
|
+
options[:width] = mediabox[2] - options[:x] + options[:width] if options[:width] <= 0
|
211
|
+
options[:height] = mediabox[3] - options[:y] + options[:height] if options[:height] <= 0
|
212
|
+
|
213
|
+
# reset the padding value
|
214
|
+
options[:text_padding] = 0 if options[:text_padding].to_f >= 1
|
215
|
+
|
216
|
+
# create box stream
|
217
|
+
box_stream = ''
|
218
|
+
# set graphic state for box
|
219
|
+
if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
|
220
|
+
# compute x and y position for text
|
221
|
+
x = options[:x]
|
222
|
+
y = options[:y]
|
223
|
+
|
224
|
+
# set graphic state for the box
|
225
|
+
box_stream << "q\n"
|
226
|
+
box_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
|
227
|
+
box_graphic_state = { ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 0, LJ: 0, LD: 0 }
|
228
|
+
if options[:box_radius] != 0 # if the text box has rounded corners
|
229
|
+
box_graphic_state[:LC] = 2
|
230
|
+
box_graphic_state[:LJ] = 1
|
231
|
+
end
|
232
|
+
box_graphic_state = graphic_state box_graphic_state # adds the graphic state to Resources and gets the reference
|
233
|
+
box_stream << "#{object_to_pdf box_graphic_state} gs\n"
|
234
|
+
|
235
|
+
# the following line was removed for Acrobat Reader compatability
|
236
|
+
# box_stream << "DeviceRGB CS\nDeviceRGB cs\n"
|
237
|
+
|
238
|
+
box_stream << "#{options[:box_color].join(' ')} rg\n" if options[:box_color]
|
239
|
+
if options[:border_width].to_i > 0 && options[:border_color]
|
240
|
+
box_stream << "#{options[:border_color].join(' ')} RG\n"
|
241
|
+
end
|
242
|
+
# create the path
|
243
|
+
radius = options[:box_radius]
|
244
|
+
half_radius = (radius.to_f / 2).round 4
|
245
|
+
## set starting point
|
246
|
+
box_stream << "#{options[:x] + radius} #{options[:y]} m\n"
|
247
|
+
## buttom and right corner - first line and first corner
|
248
|
+
box_stream << "#{options[:x] + options[:width] - radius} #{options[:y]} l\n" # buttom
|
249
|
+
if options[:box_radius] != 0 # make first corner, if not straight.
|
250
|
+
box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y]} "
|
251
|
+
box_stream << "#{options[:x] + options[:width]} #{options[:y] + half_radius} "
|
252
|
+
box_stream << "#{options[:x] + options[:width]} #{options[:y] + radius} c\n"
|
253
|
+
end
|
254
|
+
## right and top-right corner
|
255
|
+
box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - radius} l\n"
|
256
|
+
if options[:box_radius] != 0
|
257
|
+
box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - half_radius} "
|
258
|
+
box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y] + options[:height]} "
|
259
|
+
box_stream << "#{options[:x] + options[:width] - radius} #{options[:y] + options[:height]} c\n"
|
260
|
+
end
|
261
|
+
## top and top-left corner
|
262
|
+
box_stream << "#{options[:x] + radius} #{options[:y] + options[:height]} l\n"
|
263
|
+
if options[:box_radius] != 0
|
264
|
+
box_stream << "#{options[:x] + half_radius} #{options[:y] + options[:height]} "
|
265
|
+
box_stream << "#{options[:x]} #{options[:y] + options[:height] - half_radius} "
|
266
|
+
box_stream << "#{options[:x]} #{options[:y] + options[:height] - radius} c\n"
|
267
|
+
end
|
268
|
+
## left and buttom-left corner
|
269
|
+
box_stream << "#{options[:x]} #{options[:y] + radius} l\n"
|
270
|
+
if options[:box_radius] != 0
|
271
|
+
box_stream << "#{options[:x]} #{options[:y] + half_radius} "
|
272
|
+
box_stream << "#{options[:x] + half_radius} #{options[:y]} "
|
273
|
+
box_stream << "#{options[:x] + radius} #{options[:y]} c\n"
|
274
|
+
end
|
275
|
+
# fill / stroke path
|
276
|
+
box_stream << "h\n"
|
277
|
+
if options[:box_color] && options[:border_width].to_i > 0 && options[:border_color]
|
278
|
+
box_stream << "B\n"
|
279
|
+
elsif options[:box_color] # fill if fill color is set
|
280
|
+
box_stream << "f\n"
|
281
|
+
elsif options[:border_width].to_i > 0 && options[:border_color] # stroke if border is set
|
282
|
+
box_stream << "S\n"
|
283
|
+
end
|
284
|
+
|
285
|
+
# exit graphic state for the box
|
286
|
+
box_stream << "Q\n"
|
287
|
+
end
|
288
|
+
contents << box_stream
|
289
|
+
|
290
|
+
# reset x,y by text alignment - x,y are calculated from the buttom left
|
291
|
+
# each unit (1) is 1/72 Inch
|
292
|
+
# create text stream
|
293
|
+
text_stream = ''
|
294
|
+
if !text.to_s.empty? && options[:font_size] != 0 && (options[:font_color] || options[:stroke_color])
|
295
|
+
# compute x and y position for text
|
296
|
+
x = options[:x] + (options[:width] * options[:text_padding])
|
297
|
+
y = options[:y] + (options[:height] * options[:text_padding])
|
298
|
+
|
299
|
+
# set the fonts (fonts array, with :Helvetica as fallback).
|
300
|
+
fonts = [*options[:font], :Helvetica]
|
301
|
+
# fit text in box, if requested
|
302
|
+
font_size = options[:font_size]
|
303
|
+
if options[:font_size] == :fit_text
|
304
|
+
font_size = fit_text text, fonts, (options[:width] * (1 - options[:text_padding])), (options[:height] * (1 - options[:text_padding]))
|
305
|
+
font_size = options[:max_font_size] if options[:max_font_size] && font_size > options[:max_font_size]
|
306
|
+
end
|
307
|
+
|
308
|
+
text_size = dimensions_of text, fonts, font_size
|
309
|
+
|
310
|
+
if options[:text_align] == :center
|
311
|
+
x = ((options[:width] * (1 - (2 * options[:text_padding]))) - text_size[0]) / 2 + x
|
312
|
+
elsif options[:text_align] == :right
|
313
|
+
x = ((options[:width] * (1 - (1.5 * options[:text_padding]))) - text_size[0]) + x
|
314
|
+
end
|
315
|
+
if options[:text_valign] == :center
|
316
|
+
y = ((options[:height] * (1 - (2 * options[:text_padding]))) - text_size[1]) / 2 + y
|
317
|
+
elsif options[:text_valign] == :top
|
318
|
+
y = (options[:height] * (1 - (1.5 * options[:text_padding]))) - text_size[1] + y
|
319
|
+
end
|
320
|
+
|
321
|
+
# set graphic state for text
|
322
|
+
text_stream << "q\n"
|
323
|
+
text_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
|
324
|
+
text_graphic_state = graphic_state(ca: options[:opacity], CA: options[:opacity], LW: options[:stroke_width].to_f, LC: 2, LJ: 1, LD: 0)
|
325
|
+
text_stream << "#{object_to_pdf text_graphic_state} gs\n"
|
326
|
+
|
327
|
+
# the following line was removed for Acrobat Reader compatability
|
328
|
+
# text_stream << "DeviceRGB CS\nDeviceRGB cs\n"
|
329
|
+
|
330
|
+
# set text render mode
|
331
|
+
if options[:font_color]
|
332
|
+
text_stream << "#{options[:font_color].join(' ')} rg\n"
|
333
|
+
end
|
334
|
+
if options[:stroke_width].to_i > 0 && options[:stroke_color]
|
335
|
+
text_stream << "#{options[:stroke_color].join(' ')} RG\n"
|
336
|
+
if options[:font_color]
|
337
|
+
text_stream << "2 Tr\n"
|
338
|
+
else
|
339
|
+
final_stream << "1 Tr\n"
|
340
|
+
end
|
341
|
+
elsif options[:font_color]
|
342
|
+
text_stream << "0 Tr\n"
|
343
|
+
else
|
344
|
+
text_stream << "3 Tr\n"
|
345
|
+
end
|
346
|
+
# format text object(s)
|
347
|
+
# text_stream << "#{options[:font_color].join(' ')} rg\n" # sets the color state
|
348
|
+
encode_text(text, fonts).each do |encoded|
|
349
|
+
text_stream << "BT\n" # the Begine Text marker
|
350
|
+
text_stream << format_name_to_pdf(set_font(encoded[0])) # Set font name
|
351
|
+
text_stream << " #{font_size.round 3} Tf\n" # set font size and add font operator
|
352
|
+
text_stream << "#{x.round 4} #{y.round 4} Td\n" # set location for text object
|
353
|
+
text_stream << (encoded[1]) # insert the encoded string to the stream
|
354
|
+
text_stream << " Tj\n" # the Text object operator and the End Text marker
|
355
|
+
text_stream << "ET\n" # the Text object operator and the End Text marker
|
356
|
+
x += encoded[2] / 1000 * font_size # update text starting point
|
357
|
+
y -= encoded[3] / 1000 * font_size # update text starting point
|
358
|
+
end
|
359
|
+
# exit graphic state for text
|
360
|
+
text_stream << "Q\n"
|
361
|
+
end
|
362
|
+
contents << text_stream
|
363
|
+
|
364
|
+
self
|
365
|
+
end
|
366
|
+
|
367
|
+
# gets the dimentions (width and height) of the text, as it will be printed in the PDF.
|
368
|
+
#
|
369
|
+
# text:: the text to measure
|
370
|
+
# font:: a font name or an Array of font names. Font names should be registered fonts. The 14 standard fonts are pre regitered with the font library.
|
371
|
+
# size:: the size of the font (defaults to 1000 points).
|
372
|
+
def dimensions_of(text, fonts, size = 1000)
|
373
|
+
Fonts.dimensions_of text, fonts, size
|
374
|
+
end
|
375
|
+
|
376
|
+
# this method returns the size for which the text fits the requested metrices
|
377
|
+
# the size is type Float and is rather exact
|
378
|
+
# if the text cannot fit such a small place, returns zero (0).
|
379
|
+
# maximum font size possible is set to 100,000 - which should be big enough for anything
|
380
|
+
# text:: the text to fit
|
381
|
+
# font:: the font name. @see font
|
382
|
+
# length:: the length to fit
|
383
|
+
# height:: the height to fit (optional - normally length is the issue)
|
384
|
+
def fit_text(text, font, length, height = 10_000_000)
|
385
|
+
size = 100_000
|
386
|
+
size_array = [size]
|
387
|
+
metrics = Fonts.dimensions_of text, font, size
|
388
|
+
size_array << size * length / metrics[0] if metrics[0] > length
|
389
|
+
size_array << size * height / metrics[1] if metrics[1] > height
|
390
|
+
size_array.min
|
391
|
+
end
|
392
|
+
|
393
|
+
# This method moves the Page[:Rotate] property into the page's data stream, so that
|
394
|
+
# "what you see is what you get".
|
395
|
+
#
|
396
|
+
# After using thie method, {#orientation} should return the absolute orientation rather than only the data's orientation (unless `:Rotate` is changed).
|
397
|
+
#
|
398
|
+
# This is usful in cases where there might be less control over the source PDF files,
|
399
|
+
# and the user assums that the PDF page's data is the same as the PDF's pages
|
400
|
+
# on screen display (Rotate rotates a page but leaves the data in the original orientation).
|
401
|
+
#
|
402
|
+
# The method returns the page object, thus allowing method chaining (i.e. `page[:Rotate] = 90; page.textbox('hello!').fix_rotation.textbox('hello!')`)
|
403
|
+
def fix_rotation
|
404
|
+
return self if self[:Rotate].to_f == 0.0 || mediabox.nil?
|
405
|
+
# calculate the rotation
|
406
|
+
r = self[:Rotate].to_f * Math::PI / 180
|
407
|
+
s = Math.sin(r).round 6
|
408
|
+
c = Math.cos(r).round 6
|
409
|
+
ctm = [c, s, -s, c]
|
410
|
+
# calculate the translation (move the origin of x,y to the new origin).
|
411
|
+
x = mediabox[2] - mediabox[0]
|
412
|
+
y = mediabox[3] - mediabox[1]
|
413
|
+
ctm.push(((x * c).abs - x * c + (y * s).abs + y * s) / 2, ((x * s).abs - x * s + (y * c).abs - y * c) / 2)
|
414
|
+
|
415
|
+
# insert the rotation stream into the current content stream
|
416
|
+
insert_content "q\n#{ctm.join ' '} cm\n", 0
|
417
|
+
# close the rotation stream
|
418
|
+
insert_content CONTENT_CONTAINER_END
|
419
|
+
# reset the mediabox and cropbox values - THIS IS ONLY FOR ORIENTATION CHANGE...
|
420
|
+
if (self[:Rotate].to_f / 90).to_i.odd?
|
421
|
+
self[:MediaBox] = self[:MediaBox].values_at(1, 0, 3, 2)
|
422
|
+
self[:CropBox] = self[:CropBox].values_at(1, 0, 3, 2) if self[:CropBox]
|
423
|
+
end
|
424
|
+
# reset the Rotate property
|
425
|
+
delete :Rotate
|
426
|
+
# disconnect the content stream, so that future inserts aren't rotated
|
427
|
+
@contents = false # init_contents
|
428
|
+
|
429
|
+
# always return self, for chaining.
|
430
|
+
self
|
431
|
+
end
|
432
|
+
|
433
|
+
# resizes the page relative to it's current viewport (either the cropbox or the mediabox), setting the new viewport to the requested size.
|
434
|
+
#
|
435
|
+
# accepts:
|
436
|
+
# new_size:: an Array with four elements: [X0, Y0, X_max, Y_max]. For example, A4: `[0, 0, 595, 842]`. It is important that the first two numbers are 0 unless a special effect is attempted. If the first two numbers change, the final result might not be the size requested, but the nearest possible transformation (calling the method again will allow a better resizing).
|
437
|
+
# conserve_aspect_ratio:: whether to keep the current content in the same aspect ratio or to allow streaching. Defaults to true - so that although the content is resized, it might not fill the new size completely.
|
438
|
+
def resize(new_size = nil, conserve_aspect_ratio = true)
|
439
|
+
return page_size unless new_size
|
440
|
+
c_mediabox = mediabox
|
441
|
+
c_cropbox = cropbox
|
442
|
+
c_size = c_cropbox || c_mediabox
|
443
|
+
x_ratio = 1.0 * (new_size[2] - new_size[0]) / (c_size[2]) #-c_size[0])
|
444
|
+
y_ratio = 1.0 * (new_size[3] - new_size[1]) / (c_size[3]) #-c_size[1])
|
445
|
+
x_move = new_size[0] - c_size[0]
|
446
|
+
y_move = new_size[1] - c_size[1]
|
447
|
+
# puts "ctm will be: #{x_ratio.round(4)} 0 0 #{y_ratio.round(4)} #{x_move} #{y_move}"
|
448
|
+
self[:MediaBox] = [(c_mediabox[0] + x_move), (c_mediabox[1] + y_move), ((c_mediabox[2] * x_ratio) + x_move), ((c_mediabox[3] * y_ratio) + y_move)]
|
449
|
+
self[:CropBox] = [(c_cropbox[0] + x_move), (c_cropbox[1] + y_move), ((c_cropbox[2] * x_ratio) + x_move), ((c_cropbox[3] * y_ratio) + y_move)] if c_cropbox
|
450
|
+
x_ratio = y_ratio = [x_ratio, y_ratio].min if conserve_aspect_ratio
|
451
|
+
# insert the rotation stream into the current content stream
|
452
|
+
# insert_content "q\n#{x_ratio.round(4).to_s} 0 0 #{y_ratio.round(4).to_s} 0 0 cm\n1 0 0 1 #{x_move} #{y_move} cm\n", 0
|
453
|
+
insert_content "q\n#{x_ratio.round(4)} 0 0 #{y_ratio.round(4)} #{x_move} #{y_move} cm\n", 0
|
454
|
+
# close the rotation stream
|
455
|
+
insert_content CONTENT_CONTAINER_END
|
456
|
+
# disconnect the content stream, so that future inserts aren't rotated
|
457
|
+
@contents = false # init_contents
|
458
|
+
|
459
|
+
# always return self, for chaining.
|
460
|
+
self
|
461
|
+
end
|
462
|
+
|
463
|
+
# crops the page using a <b>relative</b> size.
|
464
|
+
#
|
465
|
+
# `crop` will crop the page by updating it's MediaBox property using a <b>relative</b> crop box. i.e.,
|
466
|
+
# when cropping a page with {#page_size} of [10,10,900,900] to [5,5,500,500], the resulting page size should be [15, 15, 510, 510] - allowing you to ignore a page's initial XY starting point when cropping.
|
467
|
+
#
|
468
|
+
# for an absolute cropping, simpy use the {#mediabox=} or {#cropbox=} methods, setting their value to the new {page_size}.
|
469
|
+
#
|
470
|
+
# accepts:
|
471
|
+
# new_size:: an Array with four elements: [X0, Y0, X_max, Y_max]. For example, inch4(width)x6(length): `[200, 200, 488, 632]`
|
472
|
+
def crop(new_size = nil)
|
473
|
+
# no crop box? clear any cropping.
|
474
|
+
return page_size unless new_size
|
475
|
+
# type safety
|
476
|
+
raise TypeError, "pdf.page\#crop expeceted an Array (or nil)" unless Array === new_size
|
477
|
+
|
478
|
+
# set the MediaBox to the existing page size
|
479
|
+
self[:MediaBox] = page_size
|
480
|
+
# clear the CropBox
|
481
|
+
self[:CropBox] = nil
|
482
|
+
# update X0
|
483
|
+
self[:MediaBox][0] += new_size[0]
|
484
|
+
# update Y0
|
485
|
+
self[:MediaBox][1] += new_size[1]
|
486
|
+
# update X max IF the value is smaller then the existing value
|
487
|
+
self[:MediaBox][2] = (self[:MediaBox][0] + new_size[2] - new_size[0]) if (self[:MediaBox][0] + new_size[2] - new_size[0]) < self[:MediaBox][2]
|
488
|
+
# update Y max IF the value is smaller then the existing value
|
489
|
+
self[:MediaBox][3] = (self[:MediaBox][1] + new_size[3] - new_size[1]) if (self[:MediaBox][1] + new_size[3] - new_size[1]) < self[:MediaBox][3]
|
490
|
+
# return self for chaining
|
491
|
+
self
|
492
|
+
end
|
493
|
+
|
494
|
+
# rotate the page 90 degrees counter clockwise
|
495
|
+
def rotate_left
|
496
|
+
self[:Rotate] = self[:Rotate].to_f + 90
|
497
|
+
fix_rotation
|
498
|
+
end
|
499
|
+
|
500
|
+
# rotate the page 90 degrees clockwise
|
501
|
+
def rotate_right
|
502
|
+
self[:Rotate] = self[:Rotate].to_f - 90
|
503
|
+
fix_rotation
|
504
|
+
end
|
505
|
+
|
506
|
+
# rotate the page by 180 degrees
|
507
|
+
def rotate_180
|
508
|
+
self[:Rotate] = self[:Rotate].to_f +180
|
509
|
+
fix_rotation
|
510
|
+
end
|
511
|
+
|
512
|
+
# get or set (by clockwise rotation) the page's data orientation.
|
513
|
+
#
|
514
|
+
# note that the data's orientation is the way data is oriented on the page.
|
515
|
+
# The display orientati0n (which might different) is controlled by the `:Rotate` property. see {#fix_orientation} for more details.
|
516
|
+
#
|
517
|
+
# accepts one optional parameter:
|
518
|
+
# force:: to get the orientation, pass nil. to set the orientatiom, set fource to either :portrait or :landscape. defaults to nil (get orientation).
|
519
|
+
# clockwise:: sets the rotation directions. defaults to true (clockwise rotation).
|
520
|
+
#
|
521
|
+
# returns the current orientation (:portrait or :landscape) if used to get the orientation.
|
522
|
+
# otherwise, if used to set the orientation, returns the page object to allow method chaining.
|
523
|
+
#
|
524
|
+
# * Notice: a square page always returns the :portrait value and is ignored when trying to set the orientation.
|
525
|
+
def orientation(force = nil, clockwise = true)
|
526
|
+
a = page_size
|
527
|
+
return (a[2] - a[0] > a[3] - a[1]) ? :landscape : :portrait unless force
|
528
|
+
unless orientation == force || (a[2] - a[0] == a[3] - a[1])
|
529
|
+
self[:Rotate] = 0
|
530
|
+
clockwise ? rotate_right : rotate_left
|
531
|
+
end
|
532
|
+
self
|
533
|
+
end
|
534
|
+
|
535
|
+
# Writes a table to the current page, removing(!) the written rows from the table_data Array.
|
536
|
+
#
|
537
|
+
# since the table_data Array is updated, it is possible to call this method a few times,
|
538
|
+
# each time creating or moving to the next page, until table_data.empty? returns true.
|
539
|
+
#
|
540
|
+
# accepts a Hash with any of the following keys as well as any of the PDFWriter#textbox options:
|
541
|
+
# headers:: an Array of strings with the headers (will be repeated every page).
|
542
|
+
# table_data:: as Array of Arrays, each containing a string for each column. the first row sets the number of columns. extra columns will be ignored.
|
543
|
+
# font:: a registered or standard font name (see PDFWriter). defaults to nil (:Helvetica).
|
544
|
+
# header_font:: a registered or standard font name for the headers (see PDFWriter). defaults to nil (the font for all the table rows).
|
545
|
+
# max_font_size:: the maximum font size. if the string doesn't fit, it will be resized. defaults to 14.
|
546
|
+
# column_widths:: an array of relative column widths ([1,2] will display only the first two columns, the second twice as big as the first). defaults to nil (even widths).
|
547
|
+
# header_color:: the header color. defaults to [0.8, 0.8, 0.8] (light gray).
|
548
|
+
# main_color:: main row color. defaults to nil (transparent / white).
|
549
|
+
# alternate_color:: alternate row color. defaults to [0.95, 0.95, 0.95] (very light gray).
|
550
|
+
# font_color:: font color. defaults to [0,0,0] (black).
|
551
|
+
# border_color:: border color. defaults to [0,0,0] (black).
|
552
|
+
# border_width:: border width in PDF units. defaults to 1.
|
553
|
+
# header_align:: the header text alignment within each column (:right, :left, :center). defaults to :center.
|
554
|
+
# row_align:: the row text alignment within each column. defaults to :left (:right for RTL table).
|
555
|
+
# direction:: the table's writing direction (:ltr or :rtl). this reffers to the direction of the columns and doesn't effect text (rtl text is automatically recognized). defaults to :ltr.
|
556
|
+
# max_rows:: the maximum number of rows to actually draw, INCLUDING the header row. deafults to 25.
|
557
|
+
# xy:: an Array specifying the top-left corner of the table. defaulte to [page_width*0.1, page_height*0.9].
|
558
|
+
# size:: an Array specifying the height and the width of the table. defaulte to [page_width*0.8, page_height*0.8].
|
559
|
+
def write_table(options = {})
|
560
|
+
defaults = {
|
561
|
+
headers: nil,
|
562
|
+
table_data: [[]],
|
563
|
+
font: nil,
|
564
|
+
header_font: nil,
|
565
|
+
max_font_size: 14,
|
566
|
+
column_widths: nil,
|
567
|
+
header_color: [0.8, 0.8, 0.8],
|
568
|
+
main_color: nil,
|
569
|
+
alternate_color: [0.95, 0.95, 0.95],
|
570
|
+
font_color: [0, 0, 0],
|
571
|
+
border_color: [0, 0, 0],
|
572
|
+
border_width: 1,
|
573
|
+
header_align: :center,
|
574
|
+
row_align: nil,
|
575
|
+
direction: :ltr,
|
576
|
+
max_rows: 25,
|
577
|
+
xy: nil,
|
578
|
+
size: nil
|
579
|
+
}
|
580
|
+
options = defaults.merge options
|
581
|
+
raise 'method call error! not enough rows allowed to create table' if (options[:max_rows].to_i < 1 && options[:headers]) || (options[:max_rows].to_i <= 0)
|
582
|
+
options[:header_font] ||= options[:font]
|
583
|
+
options[:row_align] ||= ((options[:direction] == :rtl) ? :right : :left)
|
584
|
+
options[:xy] ||= [((page_size[2] - page_size[0]) * 0.1), ((page_size[3] - page_size[1]) * 0.9)]
|
585
|
+
options[:size] ||= [((page_size[2] - page_size[0]) * 0.8), ((page_size[3] - page_size[1]) * 0.8)]
|
586
|
+
# assert table_data is an array of arrays
|
587
|
+
return false unless (options[:table_data].select { |r| !r.is_a?(Array) }).empty?
|
588
|
+
# compute sizes
|
589
|
+
top = options[:xy][1]
|
590
|
+
height = options[:size][1] / options[:max_rows]
|
591
|
+
from_side = options[:xy][0]
|
592
|
+
width = options[:size][0]
|
593
|
+
columns = options[:table_data][0].length
|
594
|
+
column_widths = []
|
595
|
+
columns.times { |_i| column_widths << (width / columns) }
|
596
|
+
if options[:column_widths]
|
597
|
+
scale = 0
|
598
|
+
options[:column_widths].each { |w| scale += w }
|
599
|
+
column_widths = []
|
600
|
+
options[:column_widths].each { |w| column_widths << (width * w / scale) }
|
601
|
+
end
|
602
|
+
column_widths = column_widths.reverse if options[:direction] == :rtl
|
603
|
+
# set count and start writing the data
|
604
|
+
row_number = 1
|
605
|
+
|
606
|
+
until options[:table_data].empty? || row_number > options[:max_rows]
|
607
|
+
# add headers
|
608
|
+
if options[:headers] && row_number == 1
|
609
|
+
x = from_side
|
610
|
+
headers = options[:headers]
|
611
|
+
headers = headers.reverse if options[:direction] == :rtl
|
612
|
+
column_widths.each_index do |i|
|
613
|
+
text = headers[i].to_s
|
614
|
+
textbox text, { x: x, y: (top - (height * row_number)), width: column_widths[i], height: height, box_color: options[:header_color], text_align: options[:header_align] }.merge(options).merge(font: options[:header_font])
|
615
|
+
x += column_widths[i]
|
616
|
+
end
|
617
|
+
row_number += 1
|
618
|
+
end
|
619
|
+
x = from_side
|
620
|
+
row_data = options[:table_data].shift
|
621
|
+
row_data = row_data.reverse if options[:direction] == :rtl
|
622
|
+
column_widths.each_index do |i|
|
623
|
+
text = row_data[i].to_s
|
624
|
+
box_color = (options[:alternate_color] && ((row_number.odd? && options[:headers]) || row_number.even?)) ? options[:alternate_color] : options[:main_color]
|
625
|
+
textbox text, { x: x, y: (top - (height * row_number)), width: column_widths[i], height: height, box_color: box_color, text_align: options[:row_align] }.merge(options)
|
626
|
+
x += column_widths[i]
|
627
|
+
end
|
628
|
+
row_number += 1
|
629
|
+
end
|
630
|
+
self
|
631
|
+
end
|
632
|
+
|
633
|
+
# creates a copy of the page. if the :secure flag is set to true, the resource indentifiers (fonts etc') will be renamed in order to secure their uniqueness.
|
634
|
+
def copy(secure = false)
|
635
|
+
# since only the Content streams are modified (Resource hashes are created anew),
|
636
|
+
# it should be safe (and a lot faster) to create a deep copy only for the content hashes and streams.
|
637
|
+
delete :Parent
|
638
|
+
prep_content_array
|
639
|
+
page_copy = clone
|
640
|
+
page_copy[:Contents] = page_copy[:Contents].map do |obj|
|
641
|
+
obj = obj.dup
|
642
|
+
obj[:referenced_object] = obj[:referenced_object].dup if obj[:referenced_object]
|
643
|
+
obj[:referenced_object][:raw_stream_content] = obj[:referenced_object][:raw_stream_content].dup if obj[:referenced_object] && obj[:referenced_object][:raw_stream_content]
|
644
|
+
obj
|
645
|
+
end
|
646
|
+
if page_copy[:Resources]
|
647
|
+
page_res = page_copy[:Resources] = page_copy[:Resources].dup
|
648
|
+
page_res = page_copy[:Resources][:referenced_object] = page_copy[:Resources][:referenced_object].dup if page_copy[:Resources][:referenced_object]
|
649
|
+
page_res.each do |k, v|
|
650
|
+
v = page_res[k] = v.dup if v.is_a?(Array) || v.is_a?(Hash)
|
651
|
+
v = v[:referenced_object] = v[:referenced_object].dup if v.is_a?(Hash) && v[:referenced_object]
|
652
|
+
v = v[:referenced_object] = v[:referenced_object].dup if v.is_a?(Hash) && v[:referenced_object]
|
653
|
+
end
|
654
|
+
end
|
655
|
+
page_copy.instance_exec(secure || @secure_injection) { |s| secure_for_copy if s; init_contents; self }
|
656
|
+
end
|
657
|
+
|
658
|
+
###################################
|
659
|
+
# protected methods
|
660
|
+
|
661
|
+
protected
|
662
|
+
|
663
|
+
# accessor (getter) for the stream in the :Contents element of the page
|
664
|
+
# after getting the string object, you can operate on it but not replace it (use << or other String methods).
|
665
|
+
def contents
|
666
|
+
@contents ||= init_contents
|
667
|
+
end
|
668
|
+
|
669
|
+
# initializes the content stream in case it was not initialized before
|
670
|
+
def init_contents
|
671
|
+
self[:Contents] = self[:Contents][:referenced_object][:indirect_without_dictionary] if self[:Contents].is_a?(Hash) && self[:Contents][:referenced_object] && self[:Contents][:referenced_object].is_a?(Hash) && self[:Contents][:referenced_object][:indirect_without_dictionary]
|
672
|
+
self[:Contents] = [self[:Contents]] unless self[:Contents].is_a?(Array)
|
673
|
+
self[:Contents].delete(is_reference_only: true, referenced_object: { indirect_reference_id: 0, raw_stream_content: '' })
|
674
|
+
# un-nest any referenced arrays
|
675
|
+
self[:Contents].map! { |s| actual_value(s).is_a?(Array) ? actual_value(s) : s }
|
676
|
+
self[:Contents].flatten!
|
677
|
+
self[:Contents].compact!
|
678
|
+
# wrap content streams
|
679
|
+
insert_content 'q', 0
|
680
|
+
insert_content 'Q'
|
681
|
+
|
682
|
+
# Prep content
|
683
|
+
@contents = ''
|
684
|
+
insert_content @contents
|
685
|
+
@contents
|
686
|
+
end
|
687
|
+
|
688
|
+
# adds a string or an object to the content stream, at the location indicated
|
689
|
+
#
|
690
|
+
# accepts:
|
691
|
+
# object:: can be a string or a hash object
|
692
|
+
# location:: can be any numeral related to the possition in the :Contents array. defaults to -1 == insert at the end.
|
693
|
+
def insert_content(object, location = -1)
|
694
|
+
object = { is_reference_only: true, referenced_object: { indirect_reference_id: 0, raw_stream_content: object } } if object.is_a?(String)
|
695
|
+
raise TypeError, 'expected a String or Hash object.' unless object.is_a?(Hash)
|
696
|
+
prep_content_array
|
697
|
+
self[:Contents].insert location, object
|
698
|
+
self[:Contents].flatten!
|
699
|
+
self
|
700
|
+
end
|
701
|
+
|
702
|
+
def prep_content_array
|
703
|
+
return self if self[:Contents].is_a?(Array)
|
704
|
+
init_contents
|
705
|
+
# self[:Contents] = self[:Contents][:referenced_object] if self[:Contents].is_a?(Hash) && self[:Contents][:referenced_object] && self[:Contents][:referenced_object].is_a?(Array)
|
706
|
+
# self[:Contents] = self[:Contents][:indirect_without_dictionary] if self[:Contents].is_a?(Hash) && self[:Contents][:indirect_without_dictionary] && self[:Contents][:indirect_without_dictionary].is_a?(Array)
|
707
|
+
# self[:Contents] = [self[:Contents]] unless self[:Contents].is_a?(Array)
|
708
|
+
# self[:Contents].compact!
|
709
|
+
self
|
710
|
+
end
|
711
|
+
|
712
|
+
# returns the basic font name used internally
|
713
|
+
def base_font_name
|
714
|
+
@base_font_name ||= 'Writer' + SecureRandom.hex(7) + 'PDF'
|
715
|
+
end
|
716
|
+
|
717
|
+
# creates a font object and adds the font to the resources dictionary
|
718
|
+
# returns the name of the font for the content stream.
|
719
|
+
# font:: a Symbol of one of the fonts registered in the library, or:
|
720
|
+
# - :"Times-Roman"
|
721
|
+
# - :"Times-Bold"
|
722
|
+
# - :"Times-Italic"
|
723
|
+
# - :"Times-BoldItalic"
|
724
|
+
# - :Helvetica
|
725
|
+
# - :"Helvetica-Bold"
|
726
|
+
# - :"Helvetica-BoldOblique"
|
727
|
+
# - :"Helvetica- Oblique"
|
728
|
+
# - :Courier
|
729
|
+
# - :"Courier-Bold"
|
730
|
+
# - :"Courier-Oblique"
|
731
|
+
# - :"Courier-BoldOblique"
|
732
|
+
# - :Symbol
|
733
|
+
# - :ZapfDingbats
|
734
|
+
def set_font(font = :Helvetica)
|
735
|
+
# if the font exists, return it's name
|
736
|
+
resources[:Font] ||= {}
|
737
|
+
fonts_res = resources[:Font][:referenced_object] || resources[:Font]
|
738
|
+
fonts_res.each do |k, v|
|
739
|
+
return k if v.is_a?(Fonts::Font) && v.name && v.name == font
|
740
|
+
end
|
741
|
+
# set a secure name for the font
|
742
|
+
name = (base_font_name + (fonts_res.length + 1).to_s).to_sym
|
743
|
+
# get font object
|
744
|
+
font_object = Fonts.get_font(font)
|
745
|
+
# return false if the font wan't found in the library.
|
746
|
+
return false unless font_object
|
747
|
+
# add object to reasource
|
748
|
+
fonts_res[name] = font_object
|
749
|
+
# return name
|
750
|
+
name
|
751
|
+
end
|
752
|
+
|
753
|
+
# register or get a registered graphic state dictionary.
|
754
|
+
# the method returns the name of the graphos state, for use in a content stream.
|
755
|
+
def graphic_state(graphic_state_dictionary = {})
|
756
|
+
# if the graphic state exists, return it's name
|
757
|
+
resources[:ExtGState] ||= {}
|
758
|
+
gs_res = resources[:ExtGState][:referenced_object] || resources[:ExtGState]
|
759
|
+
gs_res.each do |k, v|
|
760
|
+
return k if v.is_a?(Hash) && v == graphic_state_dictionary
|
761
|
+
end
|
762
|
+
# set graphic state type
|
763
|
+
graphic_state_dictionary[:Type] = :ExtGState
|
764
|
+
# set a secure name for the graphic state
|
765
|
+
name = SecureRandom.hex(9).to_sym
|
766
|
+
# add object to reasource
|
767
|
+
gs_res[name] = graphic_state_dictionary
|
768
|
+
# return name
|
769
|
+
name
|
770
|
+
end
|
771
|
+
|
772
|
+
# encodes the text in an array of [:font_name, <PDFHexString>] for use in textbox
|
773
|
+
def encode_text(text, fonts)
|
774
|
+
# text must be a unicode string and fonts must be an array.
|
775
|
+
# this is an internal method, don't perform tests.
|
776
|
+
fonts_array = []
|
777
|
+
fonts.each do |name|
|
778
|
+
f = Fonts.get_font name
|
779
|
+
fonts_array << f if f
|
780
|
+
end
|
781
|
+
|
782
|
+
# before starting, we should reorder any RTL content in the string
|
783
|
+
text = reorder_rtl_content text
|
784
|
+
|
785
|
+
out = []
|
786
|
+
text.chars.each do |c|
|
787
|
+
fonts_array.each_index do |i|
|
788
|
+
next unless fonts_array[i].cmap.nil? || (fonts_array[i].cmap && fonts_array[i].cmap[c])
|
789
|
+
# add to array
|
790
|
+
if out.last.nil? || out.last[0] != fonts[i]
|
791
|
+
out.last[1] << '>' unless out.last.nil?
|
792
|
+
out << [fonts[i], '<', 0, 0]
|
793
|
+
end
|
794
|
+
out.last[1] << (fonts_array[i].cmap.nil? ? (c.unpack('H*')[0]) : fonts_array[i].cmap[c])
|
795
|
+
if fonts_array[i].metrics[c]
|
796
|
+
out.last[2] += fonts_array[i].metrics[c][:wx].to_f
|
797
|
+
out.last[3] += fonts_array[i].metrics[c][:wy].to_f
|
798
|
+
end
|
799
|
+
break
|
800
|
+
end
|
801
|
+
end
|
802
|
+
out.last[1] << '>' if out.last
|
803
|
+
out
|
804
|
+
end
|
805
|
+
|
806
|
+
# a very primitive text reordering algorithm... I was lazy...
|
807
|
+
# ...still, it works (I think).
|
808
|
+
def reorder_rtl_content(text)
|
809
|
+
rtl_characters = "\u05d0-\u05ea\u05f0-\u05f4\u0600-\u06ff\u0750-\u077f"
|
810
|
+
rtl_replaces = { '(' => ')', ')' => '(',
|
811
|
+
'[' => ']', ']' => '[',
|
812
|
+
'{' => '}', '}' => '{',
|
813
|
+
'<' => '>', '>' => '<' }
|
814
|
+
return text unless text =~ /[#{rtl_characters}]/
|
815
|
+
|
816
|
+
out = []
|
817
|
+
scanner = StringScanner.new text
|
818
|
+
until scanner.eos?
|
819
|
+
if scanner.scan /[#{rtl_characters} ]/
|
820
|
+
out.unshift scanner.matched
|
821
|
+
elsif scanner.scan /[^#{rtl_characters}]+/
|
822
|
+
if out.empty? && scanner.matched.match(/[\s]$/) && !scanner.eos?
|
823
|
+
white_space_to_move = scanner.matched.match(/[\s]+$/).to_s
|
824
|
+
out.unshift scanner.matched[0..-1 - white_space_to_move.length]
|
825
|
+
out.unshift white_space_to_move
|
826
|
+
elsif scanner.matched =~ /^[\(\)\[\]\{\}\<\>]$/
|
827
|
+
out.unshift rtl_replaces[scanner.matched]
|
828
|
+
else
|
829
|
+
out.unshift scanner.matched
|
830
|
+
end
|
831
|
+
end
|
832
|
+
end
|
833
|
+
out.join.strip
|
834
|
+
end
|
835
|
+
|
836
|
+
# copy_and_secure_for_injection(page)
|
837
|
+
# - page is a page in the pages array, i.e.
|
838
|
+
# pdf.pages[0]
|
839
|
+
# takes a page object and:
|
840
|
+
#
|
841
|
+
# makes a deep copy of the page (Ruby defaults to pointers, so this will copy the memory).
|
842
|
+
#
|
843
|
+
# then it will rewrite the content stream with renamed resources, so as to avoid name conflicts.
|
844
|
+
def secure_for_copy
|
845
|
+
# initiate dictionary from old names to new names
|
846
|
+
names_dictionary = {}
|
847
|
+
|
848
|
+
# travel every dictionary to pick up names (keys), change them and add them to the dictionary
|
849
|
+
res = resources
|
850
|
+
res.each do |k, v|
|
851
|
+
next unless actual_value(v).is_a?(Hash)
|
852
|
+
# if k == :XObject
|
853
|
+
# self[:Resources][k] = v.dup
|
854
|
+
# next
|
855
|
+
# end
|
856
|
+
new_dictionary = {}
|
857
|
+
new_name = 'Combine' + SecureRandom.hex(7) + 'PDF'
|
858
|
+
i = 1
|
859
|
+
actual_value(v).each do |old_key, value|
|
860
|
+
new_key = (new_name + i.to_s).to_sym
|
861
|
+
names_dictionary[old_key] = new_key
|
862
|
+
new_dictionary[new_key] = value
|
863
|
+
i += 1
|
864
|
+
end
|
865
|
+
res[k] = new_dictionary
|
866
|
+
end
|
867
|
+
|
868
|
+
# now that we have replaced the names in the resources dictionaries,
|
869
|
+
# it is time to replace the names inside the stream
|
870
|
+
# we will need to make sure we have access to the stream injected
|
871
|
+
# we will user PDFFilter.inflate_object
|
872
|
+
self[:Contents].each do |c|
|
873
|
+
stream = actual_value(c)
|
874
|
+
PDFFilter.inflate_object stream
|
875
|
+
names_dictionary.each do |old_key, new_key|
|
876
|
+
stream[:raw_stream_content].gsub! object_to_pdf(old_key), object_to_pdf(new_key) ##### PRAY(!) that the parsed datawill be correctly reproduced!
|
877
|
+
end
|
878
|
+
# # # the following code isn't needed now that we wrap both the existing and incoming content streams.
|
879
|
+
# # patch back to PDF defaults, for OCRed PDF files.
|
880
|
+
# stream[:raw_stream_content] = "q\n0 0 0 rg\n0 0 0 RG\n0 Tr\n1 0 0 1 0 0 cm\n%s\nQ\n" % stream[:raw_stream_content]
|
881
|
+
end
|
882
|
+
self
|
883
|
+
end
|
884
|
+
|
885
|
+
# @return [true, false] returns true if there are two different resources sharing the same named reference.
|
886
|
+
def should_secure?(page)
|
887
|
+
# travel every dictionary to pick up names (keys), change them and add them to the dictionary
|
888
|
+
res = actual_value(resources)
|
889
|
+
foreign_res = actual_value(page.resources)
|
890
|
+
tmp = nil
|
891
|
+
res.each do |k, v|
|
892
|
+
next unless (v = actual_value(v)).is_a?(Hash) && (tmp = actual_value(foreign_res[k])).is_a?(Hash)
|
893
|
+
v.keys.each do |name|
|
894
|
+
return true if tmp[name] && tmp[name] != v[name]
|
895
|
+
end # else # Do nothing, this is taken care of elseware
|
896
|
+
end
|
897
|
+
false
|
898
|
+
end
|
899
|
+
end
|
950
900
|
end
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|