1
2
3
4
5
6
7
8
9
10
11
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
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
|
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.examples.ss;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.ClientAnchor.AnchorType;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.util.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Locale;
/**
* Demonstrates how to add an image to a worksheet and set that images size
* to a specific number of millimetres irrespective of the width of the columns
* or height of the rows. Overridden methods are provided so that the location
* of the image - the cells row and column coordinates that define the top
* left hand corners of the image - can be identified either in the familiar
* Excel manner - A1 for instance - or using POI's methodology of a column and
* row index where 0, 0 would indicate cell A1.
* <p>
* The best way to make use of these techniques is to delay adding the image to
* the sheet until all other work has been completed. That way, the sizes of
* all rows and columns will have been adjusted - assuming that step was
* necessary. Even though the anchors type is set to prevent the image moving
* or re-sizing, this setting does not have any effect until the sheet is being
* viewed using the Excel application.
* <p>
* The key to the process is the ClientAnchor class. It defines methods that allow
* us to define the location of an image by specifying the following;
* <p>
* * How far - in terms of coordinate positions - the image should be inset
* from the left hand border of a cell.
* * How far - in terms of coordinate positions - the image should be inset
* from the from the top of the cell.
* * How far - in terms of coordinate positions - the right hand edge of
* the image should protrude into a cell (measured from the cells left hand
* edge to the images right hand edge).
* * How far - in terms of coordinate positions - the bottom edge of the
* image should protrude into a row (measured from the cells top edge to
* the images bottom edge).
* * The index of the column that contains the cell whose top left hand
* corner should be aligned with the top left hand corner of the image.
* * The index of the row that contains the cell whose top left hand corner
* should be aligned with the images top left hand corner.
* * The index of the column that contains the cell whose top left hand
* corner should be aligned with the images bottom right hand corner
* * The index number of the row that contains the cell whose top left
* hand corner should be aligned with the images bottom right hand corner.
* <p>
* It can be used to add an image into cell A1, for example, in the following
* manner;
* <p>
* ClientAnchor anchor = sheet.getWorkbook().getCreationHelper().createClientAnchor();
* <p>
* anchor.setDx1(0);
* anchor.setDy1(0);
* anchor.setDx2(0);
* anchor.setDy2(0);
* anchor.setCol1(0);
* anchor.setRow1(0);
* anchor.setCol2(1);
* anchor.setRow2(1);
* <p>
* Taken together, the first four methods define the locations of the top left
* and bottom right hand corners of the image if you imagine that the image is
* represented by a simple rectangle. The setDx1() and setDy1() methods locate
* the top left hand corner of the image while setDx2() and and Dy2() locate the
* bottom right hand corner of the image. An individual image can be inserted
* into a single cell or is can lie across many cells and the latter four methods
* are used to define just where the image should be positioned. They do this by
* again by identifying where the top left and bottom right hand corners of the
* image should be located but this time in terms of the indexes of the cells
* in which those corners should be located. The setCol1() and setRow1() methods
* together identify the cell that should contain the top left hand corner of
* the image while setCol2() and setRow2() do the same for the images bottom
* right hand corner.
* <p>
* Knowing that, it is possible to look again at the example above and to see
* that the top left hand corner of the image will be located in cell A1 (0, 0)
* and it will be aligned with the very top left hand corner of the cell. Likewise,
* the bottom right hand corner of the image will be located in cell B2 (1, 1) and
* it will again be aligned with the top left hand corner of the cell. This has the
* effect of making the image seem to occupy the whole of cell A1. Interestingly, it
* also has an effect on the images resizing behaviour because testing has
* demonstrated that if the image is wholly contained within one cell and is not
* 'attached' for want of a better word, to a neighbouring cell, then that image
* will not increase in size in response to the user dragging the column wider
* or the cell higher.
* <p>
* The following example demonstrates a slightly different way to insert an
* image into cell A1 and to ensure that it occupies the whole of the cell. This
* is accomplished by specifying the images bottom right hand corner should be
* aligned with the bottom right hand corner of the cell. It is also a case
* where the image will not increase in size if the user increases the size of
* the enclosing cell - irrespective of the anchors type - but it will reduce in
* size if the cell is made smaller.
* <p>
* ClientAnchor anchor = sheet.getWorkbook().getCreationHelper().createClientAnchor();
* <p>
* anchor.setDx1(0);
* anchor.setDy1(0);
* anchor.setDx2(1023);
* anchor.setDy2(255);
* anchor.setCol1(0);
* anchor.setRow1(0);
* anchor.setCol2(0);
* anchor.setRow2(0);
* <p>
* Note that the final four method calls all pass the same value and seem to
* indicate that the images top left hand corner is aligned with the top left
* hand corner of cell A1 and that its bottom right hand corner is also
* aligned with the top left hand corner of cell A1. Yet, running this code
* would see the image fully occupying cell A1. That is the result of the
* values passed to parameters three and four; these I have referred to as
* determining the images coordinates within the cell. They indicate that the
* image should occupy - in order - the full width of the column and the full
* height of the row.
* <p>
* The co-ordinate values shown are the maxima; and they are independent of
* row height/column width and of the font used. Passing 255 will always result
* in the image occupying the full height of the row and passing 1023 will
* always result in the image occupying the full width of the column. They help
* in situations where an image is larger than a column/row and must overlap
* into the next column/row. Using them does mean, however, that it is often
* necessary to perform conversions between Excels characters units, points,
* pixels and millimetres in order to establish how many rows/columns an image
* should occupy and just what the various insets ought to be.
* <p>
* Note that the setDx1(int) and setDy1(int) methods of the ClientAchor class
* are not made use of in the code that follows. It would be fairly trivial
* however to extend this example further and provide methods that would centre
* an image within a cell or allow the user to specify that a plain border a
* fixed number of millimetres wide should wrap around the image. Those first
* two parameters would make this sort of functionality perfectly possible.
* <p>
* Owing to the various conversions used, the actual size of the image may vary
* from that required; testing has so far found this to be in the region of
* plus or minus two millimetres. Most likely by modifying the way the
* calculations are performed - possibly using double(s) throughout and
* rounding the values at the correct point - it is likely that these errors
* could be reduced or removed.
* <p>
* A note concerning Excels image resizing behaviour. The ClientAnchor
* class contains a method called setAnchorType(int) which can be used to
* determine how Excel will resize an image in response to the user increasing
* or decreasing the dimensions of the cell containing the image. There are
* three values that can be passed to this method; 0 = To move and size the
* image with the cell, 2 = To move but don't size the image with the cell,
* 3 = To prevent the image from moving or being resized along with the cell. If
* an image is inserted using this class and placed into a single cell then if
* the setAnchorType(int) method is called and a value of either 0 or 2 passed
* to it, the resultant resizing behaviour may be a surprise. The image will not
* grow in size of the column is made wider or the row higher but it will shrink
* if the columns width or rows height are reduced.
*
* @version 1.00 5th August 2009.
* 2.00 26th February 2010.
* Ported to make use of the SS usermodel classes.
* Ability to reuse the Drawing Patriarch so that multiple images
* can be inserted without unintentionally erasing earlier images.
* Check on image type added; i.e. jpg, jpeg or png.
* The String used to contain the files name is now converted
* into a URL.
* 2.10 17th May 2012
* Corrected gross error that occurred when using the code with
* XSSF or SXSSF workbooks. In short, the code did not correctly
* calculate the size of the image(s) owing to the use of EMUs
* within the OOXML file format. That problem has largely been
* corrected although it should be mentioned that images are not
* sized with the same level of accuracy. Discrepancies of up to
* 2mm have been noted in testing. Further investigation will
* continue to rectify this issue.
*/
@SuppressWarnings({"java:S106", "java:S4823"})
public class AddDimensionedImage {
// Four constants that determine how - and indeed whether - the rows
// and columns an image may overlie should be expanded to accommodate that
// image.
// Passing EXPAND_ROW will result in the height of a row being increased
// to accommodate the image if it is not already larger. The image will
// be layed across one or more columns.
// Passing EXPAND_COLUMN will result in the width of the column being
// increased to accommodate the image if it is not already larger. The image
// will be layed across one or many rows.
// Passing EXPAND_ROW_AND_COLUMN will result in the height of the row
// bing increased along with the width of the column to accomdate the
// image if either is not already larger.
// Passing OVERLAY_ROW_AND_COLUMN will result in the image being layed
// over one or more rows and columns. No row or column will be resized,
// instead, code will determine how many rows and columns the image should
// overlie.
public static final int EXPAND_ROW = 1;
public static final int EXPAND_COLUMN = 2;
public static final int EXPAND_ROW_AND_COLUMN = 3;
public static final int OVERLAY_ROW_AND_COLUMN = 7;
// Modified to support EMU - English Metric Units - used within the OOXML
// workbooks, this multiplier is used to convert between measurements in
// millimetres and in EMUs
private static final int EMU_PER_MM = 36000;
/**
* Add an image to a worksheet.
*
* @param cellNumber A String that contains the location of the cell whose
* top left hand corner should be aligned with the top
* left hand corner of the image; for example "A1", "A2"
* etc. This is to support the familiar Excel syntax.
* Whilst images are not actually inserted into cells
* this provides a convenient method of indicating where
* the image should be positioned on the sheet.
* @param sheet A reference to the sheet that contains the cell referenced
* above.
* @param drawing An instance of the DrawingPatriarch class. This is now
* passed into the method where it was, previously, recovered
* from the sheet in order to allow multiple pictures be
* inserted. If the patriarch was not 'cached in this manner
* each time it was created any previously positioned images
* would be simply over-written.
* @param imageFile An instance of the URL class that encapsulates the name
* of and path to the image that is to be 'inserted into'
* the sheet.
* @param reqImageWidthMM A primitive double that contains the required
* width of the image in millimetres.
* @param reqImageHeightMM A primitive double that contains the required
* height of the image in millimetres.
* @param resizeBehaviour A primitive int whose value will determine how
* the code should react if the image is larger than
* the cell referenced by the cellNumber parameter.
* Four constants are provided to determine what
* should happen;
* AddDimensionedImage.EXPAND_ROW
* AddDimensionedImage.EXPAND_COLUMN
* AddDimensionedImage.EXPAND_ROW_AND_COLUMN
* AddDimensionedImage.OVERLAY_ROW_AND_COLUMN
* @throws java.io.FileNotFoundException If the file containing the image
* cannot be located.
* @throws java.io.IOException If a problem occurs whilst reading the file
* of image data.
* @throws java.lang.IllegalArgumentException If an invalid value is passed
* to the resizeBehaviour
* parameter.
*/
public void addImageToSheet(String cellNumber, Sheet sheet, Drawing<?> drawing,
URL imageFile, double reqImageWidthMM, double reqImageHeightMM,
int resizeBehaviour) throws IOException, IllegalArgumentException {
// Convert the String into column and row indices then chain the
// call to the overridden addImageToSheet() method.
CellReference cellRef = new CellReference(cellNumber);
this.addImageToSheet(cellRef.getCol(), cellRef.getRow(), sheet, drawing,
imageFile, reqImageWidthMM, reqImageHeightMM, resizeBehaviour);
}
/**
* Add an image to a worksheet.
*
* @param colNumber A primitive int that contains the index number of a
* column on the worksheet; POI column indices are zero
* based. Together with the rowNumber parameter's value,
* this parameter identifies a cell on the worksheet. The
* images top left hand corner will be aligned with the
* top left hand corner of this cell.
* @param rowNumber A primitive int that contains the index number of a row
* on the worksheet; POI row indices are zero based.
* Together with the rowNumber parameter's value, this
* parameter identifies a cell on the worksheet. The
* images top left hand corner will be aligned with the
* top left hand corner of this cell.
* @param sheet A reference to the sheet that contains the cell identified
* by the two parameters above.
* @param drawing An instance of the DrawingPatriarch class. This is now
* passed into the method where it was, previously, recovered
* from the sheet in order to allow multiple pictures be
* inserted. If the patriarch was not 'cached in this manner
* each time it was created any previously positioned images
* would be simply over-written.
* @param imageFile An instance of the URL class that encapsulates the name
* of and path to the image that is to be 'inserted into'
* the sheet.
* @param reqImageWidthMM A primitive double that contains the required
* width of the image in millimetres.
* @param reqImageHeightMM A primitive double that contains the required
* height of the image in millimetres.
* @param resizeBehaviour A primitive int whose value will determine how
* the code should react if the image is larger than
* the cell referenced by the colNumber and
* rowNumber parameters. Four constants are provided
* to determine what should happen;
* AddDimensionedImage.EXPAND_ROW
* AddDimensionedImage.EXPAND_COLUMN
* AddDimensionedImage.EXPAND_ROW_AND_COLUMN
* AddDimensionedImage.OVERLAY_ROW_AND_COLUMN
* @throws java.io.FileNotFoundException If the file containing the image
* cannot be located.
* @throws java.io.IOException If a problem occurs whilst reading the file
* of image data.
* @throws java.lang.IllegalArgumentException If an invalid value is passed
* to the resizeBehaviour
* parameter or if the extension
* of the image file indicates that
* it is of a type that cannot
* currently be added to the worksheet.
*/
public void addImageToSheet(int colNumber, int rowNumber, Sheet sheet, Drawing<?> drawing,
URL imageFile, double reqImageWidthMM, double reqImageHeightMM,
int resizeBehaviour) throws IOException,
IllegalArgumentException {
ClientAnchor anchor;
ClientAnchorDetail rowClientAnchorDetail;
ClientAnchorDetail colClientAnchorDetail;
int imageType;
// Validate the resizeBehaviour parameter.
if ((resizeBehaviour != AddDimensionedImage.EXPAND_COLUMN) &&
(resizeBehaviour != AddDimensionedImage.EXPAND_ROW) &&
(resizeBehaviour != AddDimensionedImage.EXPAND_ROW_AND_COLUMN) &&
(resizeBehaviour != AddDimensionedImage.OVERLAY_ROW_AND_COLUMN)) {
throw new IllegalArgumentException("Invalid value passed to the " +
"resizeBehaviour parameter of AddDimensionedImage.addImageToSheet()");
}
// Call methods to calculate how the image and sheet should be
// manipulated to accommodate the image; columns and then rows.
colClientAnchorDetail = this.fitImageToColumns(sheet, colNumber,
reqImageWidthMM, resizeBehaviour);
rowClientAnchorDetail = this.fitImageToRows(sheet, rowNumber,
reqImageHeightMM, resizeBehaviour);
// Having determined if and how to resize the rows, columns and/or the
// image, create the ClientAnchor object to position the image on
// the worksheet. Note how the two ClientAnchorDetail records are
// interrogated to recover the row/column co-ordinates and any insets.
// The first two parameters are not used currently but could be if the
// need arose to extend the functionality of this code by adding the
// ability to specify that a clear 'border' be placed around the image.
anchor = sheet.getWorkbook().getCreationHelper().createClientAnchor();
anchor.setDx1(0);
anchor.setDy1(0);
if (colClientAnchorDetail != null) {
anchor.setDx2(colClientAnchorDetail.getInset());
anchor.setCol1(colClientAnchorDetail.getFromIndex());
anchor.setCol2(colClientAnchorDetail.getToIndex());
}
if (rowClientAnchorDetail != null) {
anchor.setDy2(rowClientAnchorDetail.getInset());
anchor.setRow1(rowClientAnchorDetail.getFromIndex());
anchor.setRow2(rowClientAnchorDetail.getToIndex());
}
// For now, set the anchor type to do not move or resize the
// image as the size of the row/column is adjusted. This could easily
// become another parameter passed to the method. Please read the note
// above regarding the behaviour of image resizing.
anchor.setAnchorType(AnchorType.MOVE_AND_RESIZE);
// Now, add the picture to the workbook. Note that unlike the similar
// method in the HSSF Examples section, the image type is checked. First,
// the image files location is identified by interrogating the URL passed
// to the method, the images type is identified before it is added to the
// sheet.
String sURL = imageFile.toString().toLowerCase(Locale.ROOT);
if (sURL.endsWith(".png")) {
imageType = Workbook.PICTURE_TYPE_PNG;
} else if (sURL.endsWith(".jpg") || sURL.endsWith(".jpeg")) {
imageType = Workbook.PICTURE_TYPE_JPEG;
} else {
throw new IllegalArgumentException("Invalid Image file : " +
sURL);
}
int index = sheet.getWorkbook().addPicture(
IOUtils.toByteArray(imageFile.openStream()), imageType);
drawing.createPicture(anchor, index);
}
/**
* Determines whether the sheets columns should be re-sized to accommodate
* the image, adjusts the columns width if necessary and creates then
* returns a ClientAnchorDetail object that facilitates construction of
* an ClientAnchor that will fix the image on the sheet and establish
* its size.
*
* @param sheet A reference to the sheet that will 'contain' the image.
* @param colNumber A primitive int that contains the index number of a
* column on the sheet.
* @param reqImageWidthMM A primitive double that contains the required
* width of the image in millimetres
* @param resizeBehaviour A primitive int whose value will indicate how the
* width of the column should be adjusted if the
* required width of the image is greater than the
* width of the column.
* @return An instance of the ClientAnchorDetail class that will contain
* the index number of the column containing the cell whose top
* left hand corner also defines the top left hand corner of the
* image, the index number column containing the cell whose top
* left hand corner also defines the bottom right hand corner of
* the image and an inset that determines how far the right hand
* edge of the image can protrude into the next column - expressed
* as a specific number of coordinate positions.
*/
private ClientAnchorDetail fitImageToColumns(Sheet sheet, int colNumber,
double reqImageWidthMM, int resizeBehaviour) {
double colWidthMM;
double colCoordinatesPerMM;
int pictureWidthCoordinates;
ClientAnchorDetail colClientAnchorDetail = null;
// Get the colum's width in millimetres
colWidthMM = ConvertImageUnits.widthUnits2Millimetres(
(short) sheet.getColumnWidth(colNumber));
// Check that the column's width will accommodate the image at the
// required dimension. If the width of the column is LESS than the
// required width of the image, decide how the application should
// respond - resize the column or overlay the image across one or more
// columns.
if (colWidthMM < reqImageWidthMM) {
// Should the column's width simply be expanded?
if ((resizeBehaviour == AddDimensionedImage.EXPAND_COLUMN) ||
(resizeBehaviour == AddDimensionedImage.EXPAND_ROW_AND_COLUMN)) {
// Set the width of the column by converting the required image
// width from millimetres into Excel's column width units.
sheet.setColumnWidth(colNumber,
ConvertImageUnits.millimetres2WidthUnits(reqImageWidthMM));
// To make the image occupy the full width of the column, convert
// the required width of the image into co-ordinates. This value
// will become the inset for the ClientAnchorDetail class that
// is then instantiated.
if (sheet instanceof HSSFSheet) {
colWidthMM = reqImageWidthMM;
colCoordinatesPerMM = colWidthMM == 0 ? 0
: ConvertImageUnits.TOTAL_COLUMN_COORDINATE_POSITIONS / colWidthMM;
pictureWidthCoordinates = (int) (reqImageWidthMM * colCoordinatesPerMM);
} else {
pictureWidthCoordinates = (int) reqImageWidthMM * AddDimensionedImage.EMU_PER_MM;
}
colClientAnchorDetail = new ClientAnchorDetail(colNumber,
colNumber, pictureWidthCoordinates);
}
// If the user has chosen to overlay both rows and columns or just
// to expand ONLY the size of the rows, then calculate how to lay
// the image out across one or more columns.
else if ((resizeBehaviour == AddDimensionedImage.OVERLAY_ROW_AND_COLUMN) ||
(resizeBehaviour == AddDimensionedImage.EXPAND_ROW)) {
colClientAnchorDetail = this.calculateColumnLocation(sheet,
colNumber, reqImageWidthMM);
}
}
// If the column is wider than the image.
else {
if (sheet instanceof HSSFSheet) {
// Mow many co-ordinate positions are there per millimetre?
colCoordinatesPerMM = ConvertImageUnits.TOTAL_COLUMN_COORDINATE_POSITIONS /
colWidthMM;
// Given the width of the image, what should be its co-ordinate?
pictureWidthCoordinates = (int) (reqImageWidthMM * colCoordinatesPerMM);
} else {
pictureWidthCoordinates = (int) reqImageWidthMM *
AddDimensionedImage.EMU_PER_MM;
}
colClientAnchorDetail = new ClientAnchorDetail(colNumber,
colNumber, pictureWidthCoordinates);
}
return (colClientAnchorDetail);
}
/**
* Determines whether the sheets row should be re-sized to accommodate
* the image, adjusts the rows height if necessary and creates then
* returns a ClientAnchorDetail object that facilitates construction of
* a ClientAnchor that will fix the image on the sheet and establish
* its size.
*
* @param sheet A reference to the sheet that will 'contain' the image.
* @param rowNumber A primitive int that contains the index number of a
* row on the sheet.
* @param reqImageHeightMM A primitive double that contains the required
* height of the image in millimetres
* @param resizeBehaviour A primitive int whose value will indicate how the
* height of the row should be adjusted if the
* required height of the image is greater than the
* height of the row.
* @return An instance of the ClientAnchorDetail class that will contain
* the index number of the row containing the cell whose top
* left hand corner also defines the top left hand corner of the
* image, the index number of the row containing the cell whose
* top left hand corner also defines the bottom right hand
* corner of the image and an inset that determines how far the
* bottom edge of the image can protrude into the next (lower)
* row - expressed as a specific number of coordinate positions.
*/
private ClientAnchorDetail fitImageToRows(Sheet sheet, int rowNumber,
double reqImageHeightMM, int resizeBehaviour) {
Row row;
double rowHeightMM;
double rowCoordinatesPerMM;
int pictureHeightCoordinates;
ClientAnchorDetail rowClientAnchorDetail = null;
// Get the row and its height
row = sheet.getRow(rowNumber);
if (row == null) {
// Create row if it does not exist.
row = sheet.createRow(rowNumber);
}
// Get the row's height in millimetres
rowHeightMM = row.getHeightInPoints() / ConvertImageUnits.POINTS_PER_MILLIMETRE;
// Check that the row's height will accommodate the image at the required
// dimensions. If the height of the row is LESS than the required height
// of the image, decide how the application should respond - resize the
// row or overlay the image across a series of rows.
if (rowHeightMM < reqImageHeightMM) {
if ((resizeBehaviour == AddDimensionedImage.EXPAND_ROW) ||
(resizeBehaviour == AddDimensionedImage.EXPAND_ROW_AND_COLUMN)) {
row.setHeightInPoints((float) (reqImageHeightMM *
ConvertImageUnits.POINTS_PER_MILLIMETRE));
if (sheet instanceof HSSFSheet) {
rowHeightMM = reqImageHeightMM;
rowCoordinatesPerMM = rowHeightMM == 0 ? 0
: ConvertImageUnits.TOTAL_ROW_COORDINATE_POSITIONS / rowHeightMM;
pictureHeightCoordinates = (int) (reqImageHeightMM *
rowCoordinatesPerMM);
} else {
pictureHeightCoordinates = (int) (reqImageHeightMM *
AddDimensionedImage.EMU_PER_MM);
}
rowClientAnchorDetail = new ClientAnchorDetail(rowNumber,
rowNumber, pictureHeightCoordinates);
}
// If the user has chosen to overlay both rows and columns or just
// to expand ONLY the size of the columns, then calculate how to lay
// the image out ver one or more rows.
else if ((resizeBehaviour == AddDimensionedImage.OVERLAY_ROW_AND_COLUMN) ||
(resizeBehaviour == AddDimensionedImage.EXPAND_COLUMN)) {
rowClientAnchorDetail = this.calculateRowLocation(sheet,
rowNumber, reqImageHeightMM);
}
}
// Else, if the image is smaller than the space available
else {
if (sheet instanceof HSSFSheet) {
rowCoordinatesPerMM = ConvertImageUnits.TOTAL_ROW_COORDINATE_POSITIONS /
rowHeightMM;
pictureHeightCoordinates = (int) (reqImageHeightMM * rowCoordinatesPerMM);
} else {
pictureHeightCoordinates = (int) (reqImageHeightMM *
AddDimensionedImage.EMU_PER_MM);
}
rowClientAnchorDetail = new ClientAnchorDetail(rowNumber,
rowNumber, pictureHeightCoordinates);
}
return (rowClientAnchorDetail);
}
/**
* If the image is to overlie more than one column, calculations need to be
* performed to determine how many columns and whether the image will
* overlie just a part of one column in order to be presented at the
* required size.
*
* @param sheet The sheet that will 'contain' the image.
* @param startingColumn A primitive int whose value is the index of the
* column that contains the cell whose top left hand
* corner should be aligned with the top left hand
* corner of the image.
* @param reqImageWidthMM A primitive double whose value will indicate the
* required width of the image in millimetres.
* @return An instance of the ClientAnchorDetail class that will contain
* the index number of the column containing the cell whose top
* left hand corner also defines the top left hand corner of the
* image, the index number column containing the cell whose top
* left hand corner also defines the bottom right hand corner of
* the image and an inset that determines how far the right hand
* edge of the image can protrude into the next column - expressed
* as a specific number of coordinate positions.
*/
private ClientAnchorDetail calculateColumnLocation(Sheet sheet,
int startingColumn,
double reqImageWidthMM) {
ClientAnchorDetail anchorDetail;
double totalWidthMM = 0.0D;
double colWidthMM = 0.0D;
double overlapMM;
double coordinatePositionsPerMM;
int toColumn = startingColumn;
int inset;
// Calculate how many columns the image will have to
// span in order to be presented at the required size.
while (totalWidthMM < reqImageWidthMM) {
colWidthMM = ConvertImageUnits.widthUnits2Millimetres(
(short) (sheet.getColumnWidth(toColumn)));
// Note use of the cell border width constant. Testing with an image
// declared to fit exactly into one column demonstrated that its
// width was greater than the width of the column the POI returned.
// Further, this difference was a constant value that I am assuming
// related to the cell's borders. Either way, that difference needs
// to be allowed for in this calculation.
totalWidthMM += (colWidthMM + ConvertImageUnits.CELL_BORDER_WIDTH_MILLIMETRES);
toColumn++;
}
// De-rement by one the last column value.
toColumn--;
// Highly unlikely that this will be true but, if the width of a series
// of columns is exactly equal to the required width of the image, then
// simply build a ClientAnchorDetail object with an inset equal to the
// total number of co-ordinate positions available in a column, a
// from column co-ordinate (top left hand corner) equal to the value
// of the startingColumn parameter and a to column co-ordinate equal
// to the toColumn variable.
//
// Convert both values to ints to perform the test.
if ((int) totalWidthMM == (int) reqImageWidthMM) {
// A problem could occur if the image is sized to fit into one or
// more columns. If that occurs, the value in the toColumn variable
// will be in error. To overcome this, there are two options, to
// ibcrement the toColumn variable's value by one or to pass the
// total number of co-ordinate positions to the third paramater
// of the ClientAnchorDetail constructor. For no sepcific reason,
// the latter option is used below.
if (sheet instanceof HSSFSheet) {
anchorDetail = new ClientAnchorDetail(startingColumn,
toColumn, ConvertImageUnits.TOTAL_COLUMN_COORDINATE_POSITIONS);
} else {
anchorDetail = new ClientAnchorDetail(startingColumn,
toColumn, (int) reqImageWidthMM * AddDimensionedImage.EMU_PER_MM);
}
}
// In this case, the image will overlap part of another column and it is
// necessary to calculate just how much - this will become the inset
// for the ClientAnchorDetail object.
else {
// Firstly, claculate how much of the image should overlap into
// the next column.
overlapMM = reqImageWidthMM - (totalWidthMM - colWidthMM);
// When the required size is very close indded to the column size,
// the calcaulation above can produce a negative value. To prevent
// problems occuring in later caculations, this is simply removed
// be setting the overlapMM value to zero.
if (overlapMM < 0) {
overlapMM = 0.0D;
}
if (sheet instanceof HSSFSheet) {
// Next, from the columns width, calculate how many co-ordinate
// positons there are per millimetre
coordinatePositionsPerMM = (colWidthMM == 0) ? 0
: ConvertImageUnits.TOTAL_COLUMN_COORDINATE_POSITIONS / colWidthMM;
// From this figure, determine how many co-ordinat positions to
// inset the left hand or bottom edge of the image.
inset = (int) (coordinatePositionsPerMM * overlapMM);
} else {
inset = (int) overlapMM * AddDimensionedImage.EMU_PER_MM;
}
// Now create the ClientAnchorDetail object, setting the from and to
// columns and the inset.
anchorDetail = new ClientAnchorDetail(startingColumn, toColumn, inset);
}
return (anchorDetail);
}
/**
* If the image is to overlie more than one rows, calculations need to be
* performed to determine how many rows and whether the image will
* overlie just a part of one row in order to be presented at the
* required size.
*
* @param sheet The sheet that will 'contain' the image.
* @param startingRow A primitive int whose value is the index of the row
* that contains the cell whose top left hand corner
* should be aligned with the top left hand corner of
* the image.
* @param reqImageHeightMM A primitive double whose value will indicate the
* required height of the image in millimetres.
* @return An instance of the ClientAnchorDetail class that will contain
* the index number of the row containing the cell whose top
* left hand corner also defines the top left hand corner of the
* image, the index number of the row containing the cell whose top
* left hand corner also defines the bottom right hand corner of
* the image and an inset that determines how far the bottom edge
* can protrude into the next (lower) row - expressed as a specific
* number of co-ordinate positions.
*/
private ClientAnchorDetail calculateRowLocation(Sheet sheet,
int startingRow, double reqImageHeightMM) {
ClientAnchorDetail clientAnchorDetail;
Row row;
double rowHeightMM = 0.0D;
double totalRowHeightMM = 0.0D;
double overlapMM;
double rowCoordinatesPerMM;
int toRow = startingRow;
int inset;
// Step through the rows in the sheet and accumulate a total of their
// heights.
while (totalRowHeightMM < reqImageHeightMM) {
row = sheet.getRow(toRow);
// Note, if the row does not already exist on the sheet then create
// it here.
if (row == null) {
row = sheet.createRow(toRow);
}
// Get the row's height in millimetres and add to the running total.
rowHeightMM = row.getHeightInPoints() /
ConvertImageUnits.POINTS_PER_MILLIMETRE;
totalRowHeightMM += rowHeightMM;
toRow++;
}
// Owing to the way the loop above works, the rowNumber will have been
// incremented one row too far. Undo that here.
toRow--;
// Check to see whether the image should occupy an exact number of
// rows. If so, build the ClientAnchorDetail record to point
// to those rows and with an inset of the total number of co-ordinate
// position in the row.
//
// To overcome problems that can occur with comparing double values for
// equality, cast both to int(s) to truncate the value; VERY crude and
// I do not really like it!!
if ((int) totalRowHeightMM == (int) reqImageHeightMM) {
if (sheet instanceof HSSFSheet) {
clientAnchorDetail = new ClientAnchorDetail(startingRow, toRow,
ConvertImageUnits.TOTAL_ROW_COORDINATE_POSITIONS);
} else {
clientAnchorDetail = new ClientAnchorDetail(startingRow, toRow,
(int) reqImageHeightMM * AddDimensionedImage.EMU_PER_MM);
}
} else {
// Calculate how far the image will project into the next row. Note
// that the height of the last row assessed is subtracted from the
// total height of all rows assessed so far.
overlapMM = reqImageHeightMM - (totalRowHeightMM - rowHeightMM);
// To prevent an exception being thrown when the required width of
// the image is very close indeed to the column size.
if (overlapMM < 0) {
overlapMM = 0.0D;
}
if (sheet instanceof HSSFSheet) {
rowCoordinatesPerMM = (rowHeightMM == 0) ? 0
: ConvertImageUnits.TOTAL_ROW_COORDINATE_POSITIONS / rowHeightMM;
inset = (int) (overlapMM * rowCoordinatesPerMM);
} else {
inset = (int) overlapMM * AddDimensionedImage.EMU_PER_MM;
}
clientAnchorDetail = new ClientAnchorDetail(startingRow,
toRow, inset);
}
return (clientAnchorDetail);
}
/**
* The main entry point to the program. It contains code that demonstrates
* one way to use the program.
* <p>
* Note, the code is not restricted to use on new workbooks only. If an
* image is to be inserted into an existing workbook. just open that
* workbook, gat a reference to a sheet and pass that;
* <p>
* AddDimensionedImage addImage = new AddDimensionedImage();
* <p>
* File file = new File("....... Existing Workbook .......");
* FileInputStream fis = new FileInputStream(file);
* Workbook workbook = new HSSFWorkbook(fis);
* HSSFSheet sheet = workbook.getSheetAt(0);
* addImage.addImageToSheet("C3", sheet, "image.jpg", 30, 20,
* AddDimensionedImage.EXPAND.ROW);
*
* @param args the command line arguments
*/
public static void main(String[] args) throws IOException {
if (args.length < 2) {
System.err.println("Usage: AddDimensionedImage imageFile outputFile");
return;
}
final String imageFile = args[0];
final String outputFile = args[1];
try (final Workbook workbook = new HSSFWorkbook();
final FileOutputStream fos = new FileOutputStream(outputFile)) { // OR XSSFWorkbook
Sheet sheet = workbook.createSheet("Picture Test");
new AddDimensionedImage().addImageToSheet("B5", sheet, sheet.createDrawingPatriarch(),
new File(imageFile).toURI().toURL(), 100, 40,
AddDimensionedImage.EXPAND_ROW_AND_COLUMN);
workbook.write(fos);
}
}
/**
* The HSSFClientAnchor class accepts eight arguments. In order, these are;
* <p>
* * How far the left hand edge of the image is inset from the left hand
* edge of the cell
* * How far the top edge of the image is inset from the top of the cell
* * How far the right hand edge of the image is inset from the left
* hand edge of the cell
* * How far the bottom edge of the image is inset from the top of the
* cell.
* * Together, arguments five and six determine the column and row
* coordinates of the cell whose top left hand corner will be aligned
* with the images top left hand corner.
* * Together, arguments seven and eight determine the column and row
* coordinates of the cell whose top left hand corner will be aligned
* with the images bottom right hand corner.
* <p>
* An instance of the ClientAnchorDetail class provides three of the eight
* parameters, one of the coordinates for the images top left hand corner,
* one of the coordinates for the images bottom right hand corner and
* either how far the image should be inset from the top or the left hand
* edge of the cell.
*
* @version 1.00 5th August 2009.
*/
public static class ClientAnchorDetail {
private int fromIndex;
private int toIndex;
private int inset;
/**
* Create a new instance of the ClientAnchorDetail class using the
* following parameters.
*
* @param fromIndex A primitive int that contains one of the
* coordinates (row or column index) for the top left
* hand corner of the image.
* @param toIndex A primitive int that contains one of the
* coordinates (row or column index) for the bottom
* right hand corner of the image.
* @param inset A primitive int that contains a value which indicates
* how far the image should be inset from the top or the
* left hand edge of a cell.
*/
public ClientAnchorDetail(int fromIndex, int toIndex, int inset) {
this.fromIndex = fromIndex;
this.toIndex = toIndex;
this.inset = inset;
}
/**
* Get one of the number of the column or row that contains the cell
* whose top left hand corner will be aligned with the top left hand
* corner of the image.
*
* @return The value - row or column index - for one of the coordinates
* of the top left hand corner of the image.
*/
public int getFromIndex() {
return (this.fromIndex);
}
/**
* Get one of the number of the column or row that contains the cell
* whose top left hand corner will be aligned with the bottom right hand
* corner of the image.
*
* @return The value - row or column index - for one of the coordinates
* of the bottom right hand corner of the image.
*/
public int getToIndex() {
return (this.toIndex);
}
/**
* Get the images offset from the edge of a cell.
*
* @return How far either the right hand or bottom edge of the image is
* inset from the left hand or top edge of a cell.
*/
public int getInset() {
return (this.inset);
}
}
/**
* Utility methods used to convert Excels character based column and row
* size measurements into pixels and/or millimetres. The class also contains
* various constants that are required in other calculations.
*
* @version 1.01 30th July 2009.
* Added by Mark Beardsley [msb at apache.org].
* Additional constants.
* widthUnits2Millimetres() and millimetres2Units() methods.
*/
public static class ConvertImageUnits {
// Each cell conatins a fixed number of co-ordinate points; this number
// does not vary with row height or column width or with font. These two
// constants are defined below.
public static final int TOTAL_COLUMN_COORDINATE_POSITIONS = 1023;
public static final int TOTAL_ROW_COORDINATE_POSITIONS = 255;
// The resoultion of an image can be expressed as a specific number
// of pixels per inch. Displays and printers differ but 96 pixels per
// inch is an acceptable standard to beging with.
public static final int PIXELS_PER_INCH = 96;
// Cnstants that defines how many pixels and points there are in a
// millimetre. These values are required for the conversion algorithm.
public static final double PIXELS_PER_MILLIMETRES = 3.78;
public static final double POINTS_PER_MILLIMETRE = 2.83;
// The column width returned by HSSF and the width of a picture when
// positioned to exactly cover one cell are different by almost exactly
// 2mm - give or take rounding errors. This constant allows that
// additional amount to be accounted for when calculating how many
// celles the image ought to overlie.
public static final double CELL_BORDER_WIDTH_MILLIMETRES = 2.0D;
public static final short EXCEL_COLUMN_WIDTH_FACTOR = 256;
public static final int UNIT_OFFSET_LENGTH = 7;
private static final int[] UNIT_OFFSET_MAP = {0, 36, 73, 109, 146, 182, 219};
/**
* pixel units to excel width units(units of 1/256th of a character width)
*/
public static short pixel2WidthUnits(int pxs) {
int widthUnits = (EXCEL_COLUMN_WIDTH_FACTOR *
(pxs / UNIT_OFFSET_LENGTH));
widthUnits += UNIT_OFFSET_MAP[(pxs % UNIT_OFFSET_LENGTH)];
return (short) widthUnits;
}
/**
* excel width units(units of 1/256th of a character width) to pixel
* units.
*/
public static int widthUnits2Pixel(short widthUnits) {
int pixels = (widthUnits / EXCEL_COLUMN_WIDTH_FACTOR)
* UNIT_OFFSET_LENGTH;
int offsetWidthUnits = widthUnits % EXCEL_COLUMN_WIDTH_FACTOR;
pixels += Math.round(offsetWidthUnits /
((float) EXCEL_COLUMN_WIDTH_FACTOR / UNIT_OFFSET_LENGTH));
return pixels;
}
/**
* Convert Excels width units into millimetres.
*
* @param widthUnits The width of the column or the height of the
* row in Excels units.
* @return A primitive double that contains the columns width or rows
* height in millimetres.
*/
public static double widthUnits2Millimetres(short widthUnits) {
return (ConvertImageUnits.widthUnits2Pixel(widthUnits) /
ConvertImageUnits.PIXELS_PER_MILLIMETRES);
}
/**
* Convert into millimetres Excels width units..
*
* @param millimetres A primitive double that contains the columns
* width or rows height in millimetres.
* @return A primitive int that contains the columns width or rows
* height in Excels units.
*/
public static int millimetres2WidthUnits(double millimetres) {
return (ConvertImageUnits.pixel2WidthUnits((int) (millimetres *
ConvertImageUnits.PIXELS_PER_MILLIMETRES)));
}
}
}
|