2121 */
2222package com .koadweb .javafpdf ;
2323
24+ import com .koadweb .javafpdf .util .Compressor ;
2425import java .awt .color .ColorSpace ;
2526import java .awt .image .BufferedImage ;
27+ import java .io .ByteArrayInputStream ;
28+ import java .io .ByteArrayOutputStream ;
2629import java .io .File ;
27- import java .io .FileInputStream ;
2830import java .io .FileOutputStream ;
2931import java .io .IOException ;
3032import java .io .InputStream ;
3133import java .io .OutputStream ;
3234import java .io .UnsupportedEncodingException ;
35+ import java .nio .file .Files ;
3336import java .util .ArrayList ;
3437import java .util .Calendar ;
3538import java .util .HashMap ;
3639import java .util .List ;
3740import java .util .Locale ;
3841import java .util .Map ;
3942import java .util .Set ;
40-
4143import javax .imageio .ImageIO ;
4244
43- import com .koadweb .javafpdf .util .Compressor ;
44-
4545/**
4646 * Faithful Java port of <a href="http://www.fpdf.org">FPDF for PHP</a>.
4747 *
@@ -592,12 +592,12 @@ protected void _out(final String s) {
592592 }
593593 }
594594
595- protected Map <String , Object > _parsejpg (final File file ) {
595+ protected Map <String , Object > _parsejpg (String fileName , byte [] data ) {
596596 BufferedImage img = null ;
597597 try {
598- img = ImageIO .read (file );
598+ img = ImageIO .read (new ByteArrayInputStream ( data ) );
599599
600- Map <String , Object > image = new HashMap <String , Object >();
600+ Map <String , Object > image = new HashMap <>();
601601 image .put ("w" , Integer .valueOf (img .getWidth ()));
602602 image .put ("h" , Integer .valueOf (img .getHeight ()));
603603 String colspace ;
@@ -615,28 +615,25 @@ protected Map<String, Object> _parsejpg(final File file) {
615615 image .put ("f" , "DCTDecode" );
616616 image .put ("i" , Integer .valueOf (this .images .size () + 1 ));
617617
618- InputStream f = new FileInputStream (file );
619- byte [] data = new byte [f .available ()];
620- f .read (data , 0 , f .available ());
621- f .close ();
622- image .put ("data" , data );
618+ ByteArrayOutputStream boas = new ByteArrayOutputStream ();
619+ ImageIO .write (img , "jpg" , boas );
620+ image .put ("data" , boas .toByteArray ());
623621 return image ;
624622 } catch (IOException e ) {
625623 throw new RuntimeException (e );
626624 }
627625 }
628626
629627 /** Extract info from a PNG file */
630- protected Map <String , Object > _parsepng (final File file ) throws IOException {
631- InputStream f = new FileInputStream (file );
632- try {
628+ protected Map <String , Object > _parsepng (String fileName , byte [] imageData ) throws IOException {
629+ try (ByteArrayInputStream f = new ByteArrayInputStream (imageData )) {
633630 // Check signature
634631 char [] sig = new char [] { 137 , 'P' , 'N' , 'G' , 13 , 10 , 26 , 10 };
635632 for (int i = 0 ; i < sig .length ; i ++) {
636633 int in = f .read ();
637634 char c = (char ) in ;
638635 if (c != sig [i ]) {
639- throw new IOException ("Not a PNG file: " + file );
636+ throw new IOException ("Not a PNG file: " + fileName );
640637 }
641638 }
642639 this ._fread (f , 4 );
@@ -646,14 +643,14 @@ protected Map<String, Object> _parsepng(final File file) throws IOException {
646643 int in = f .read ();
647644 char c = (char ) in ;
648645 if (c != chunk [i ]) {
649- throw new IOException ("Not a PNG file: " + file );
646+ throw new IOException ("Not a PNG file: " + fileName );
650647 }
651648 }
652649 int w = this ._freadint (f );
653650 int h = this ._freadint (f );
654651 int bpc = f .read ();
655652 if (bpc > 8 ) {
656- throw new IOException ("16-bit depth not supported: " + file );
653+ throw new IOException ("16-bit depth not supported: " + fileName );
657654 }
658655 int ct = f .read ();
659656 String colspace ;
@@ -662,18 +659,21 @@ protected Map<String, Object> _parsepng(final File file) throws IOException {
662659 } else if (ct == 2 ) {
663660 colspace = "DeviceRGB" ;
664661 } else if (ct == 3 ) {
665- colspace = "Indexed" ;
662+ colspace = "Indexed" ;
663+ } else if (ct == 6 ) {
664+ // RGBA needs handled separately
665+ return _parsepngWithAlpha (fileName , imageData );
666666 } else {
667- throw new IOException ("Alpha channel not supported: " + file );
667+ throw new IOException ("Alpha channel not supported for grayscale PNG images : " + fileName );
668668 }
669669 if (f .read () != 0 ) {
670- throw new IOException ("Unknown compression method: " + file );
670+ throw new IOException ("Unknown compression method: " + fileName );
671671 }
672672 if (f .read () != 0 ) {
673- throw new IOException ("Unknown filter method: " + file );
673+ throw new IOException ("Unknown filter method: " + fileName );
674674 }
675675 if (f .read () != 0 ) {
676- throw new IOException ("Interlacing not supported: " + file );
676+ throw new IOException ("Interlacing not supported: " + fileName );
677677 }
678678 this ._fread (f , 4 );
679679 StringBuilder sb = new StringBuilder ();
@@ -717,7 +717,7 @@ protected Map<String, Object> _parsepng(final File file) throws IOException {
717717 }
718718 } while (f .available () > 0 );
719719 if (colspace .equals ("Indexed" ) && (pal == null )) {
720- throw new IOException ("Missing palette in " + file );
720+ throw new IOException ("Missing palette in " + fileName );
721721 }
722722 Map <String , Object > image = new HashMap <String , Object >();
723723 image .put ("w" , Integer .valueOf (w ));
@@ -731,10 +731,47 @@ protected Map<String, Object> _parsepng(final File file) throws IOException {
731731 image .put ("data" , data );
732732 image .put ("i" , Integer .valueOf (this .images .size () + 1 ));
733733 return image ;
734- } finally {
735- f .close ();
736734 }
737735 }
736+
737+ /** Parse a PNG file with an alpha channel */
738+ protected Map <String , Object > _parsepngWithAlpha (String fileName , byte [] data ) throws IOException {
739+ BufferedImage img = ImageIO .read (new ByteArrayInputStream (data ));
740+ int width = img .getWidth ();
741+ int height = img .getHeight ();
742+
743+ // PNG files with alpha channel can only have 8 or 16 bit depth
744+ // we can't handle 16 bits, so that leaves a byte. we only need grayscale for the mask image
745+
746+ int [] imgPx = img .getRGB (0 , 0 , width , height , null , 0 , width );
747+ int [] maskPx = new int [width *height ];
748+
749+ // Split alpha channel off into a grayscale image
750+ for (int i = 0 ; i < imgPx .length ; i ++) {
751+ int a = (imgPx [i ] >> 24 ) & 0xFF ; // AARRGGBB -> XXXXXXAA -> 000000AA;
752+ maskPx [i ] = a | a << 8 | a << 16 ; // 000000AA | 0000AA00 | 00AA0000 -> 00AAAAAA
753+ imgPx [i ] = imgPx [i ] & 0x00FFFFFF ; // AARRGGBB -> 00RRGGBB
754+ }
755+
756+ // out contains the original image, stripped of the alpha channel
757+ BufferedImage out = new BufferedImage (width , height , BufferedImage .TYPE_INT_RGB );
758+ out .setRGB (0 , 0 , width , height , imgPx , 0 , width );
759+
760+ // mask contains the grayscale-converted alpha channel of the original image
761+ BufferedImage mask = new BufferedImage (width , height , BufferedImage .TYPE_BYTE_GRAY );
762+ mask .setRGB (0 , 0 , width , height , maskPx , 0 , width );
763+
764+ // attempt to re-parse the image, but without the alpha channel
765+ ByteArrayOutputStream baos = new ByteArrayOutputStream ();
766+ ImageIO .write (out , "png" , baos );
767+ Map <String , Object > info = _parsepng (fileName , baos .toByteArray ());
768+
769+ // attach the alpha mask to the image info for use later on
770+ baos .reset ();
771+ ImageIO .write (mask , "png" , baos );
772+ info .put ("alphaMask" , baos .toByteArray ());
773+ return info ;
774+ }
738775
739776 protected void _putcatalog () {
740777 this ._out ("/Type /Catalog" );
@@ -798,6 +835,11 @@ protected void _putimages() {
798835 + this .images .get (file ).get ("w" ));
799836 this ._out ("/Height "
800837 + this .images .get (file ).get ("h" ));
838+
839+ if (this .images .get (file ).containsKey ("alphaMask" )) {
840+ this ._out ("/SMask " + (Integer )this .images .get ("alphaMask-" + file ).get ("n" ) + " 0 R" );
841+ }
842+
801843 if (this .images .get (file ).get ("cs" ) == "Indexed" ) {
802844 this ._out ("/ColorSpace [/Indexed /DeviceRGB "
803845 + (((byte []) this .images .get (file ).get ("pal" )).length / 3 - 1 ) + " " + (this .n + 1 ) + " 0 R]" );
@@ -1816,6 +1858,13 @@ public float getY() {
18161858 */
18171859 public void Image (final String file , final Coordinate coords , final float w , final float h , final ImageType type ,
18181860 final int link ) throws IOException {
1861+ File f = new File (file );
1862+ Image (file , Files .readAllBytes (f .toPath ()), coords , w , h , type , link , false );
1863+ }
1864+
1865+ @ SuppressWarnings ("fallthrough" )
1866+ protected void Image (final String file , byte [] data , Coordinate coords , final float w , final float h , final ImageType type ,
1867+ final int link , boolean isMask ) throws IOException {
18191868 Map <String , Object > info = null ;
18201869 if (this .images .get (file ) == null ) {
18211870 // First use of image, get info
@@ -1830,18 +1879,39 @@ public void Image(final String file, final Coordinate coords, final float w, fin
18301879 } else {
18311880 type1 = type ;
18321881 }
1833- if (ImageType .PNG .equals (type1 )) {
1834- info = this ._parsepng (new File (file ));
1835- } else if (ImageType .JPEG .equals (type1 )) {
1836- info = this ._parsejpg (new File (file ));
1837- } else {
1838- throw new IOException ("Image type not supported." );
1882+
1883+ switch (type1 ) {
1884+ case GIF :
1885+ // gifs: convert to png first
1886+ ByteArrayOutputStream baos = new ByteArrayOutputStream ();
1887+ ImageIO .write (ImageIO .read (new ByteArrayInputStream (data )), "png" , baos );
1888+ data = baos .toByteArray ();
1889+ // fallthrough!
1890+ case PNG :
1891+ info = this ._parsepng (file , data );
1892+ break ;
1893+ case JPEG :
1894+ info = this ._parsejpg (file , data );
1895+ break ;
1896+ default :
1897+ throw new IOException ("Image type not supported." );
18391898 }
18401899 // FIXME no support for other formats
18411900 this .images .put (file , info );
18421901 } else {
18431902 info = this .images .get (file );
18441903 }
1904+
1905+ // if the image has an alpha mask, add it separately
1906+ if (info .containsKey ("alphaMask" )) {
1907+ this .Image ("alphaMask-" + file , (byte [])info .get ("alphaMask" ), new Coordinate (0 , 0 ), 0 , 0 , ImageType .PNG , 0 , true );
1908+ }
1909+
1910+ // masks are grayscale, regardless of what it claims
1911+ if (isMask ) {
1912+ info .put ("cs" , "DeviceGray" );
1913+ }
1914+
18451915 // Automatic width and height calculation if needed
18461916 float w1 = w ;
18471917 float h1 = h ;
@@ -1856,6 +1926,15 @@ public void Image(final String file, final Coordinate coords, final float w, fin
18561926 h1 = w * ((Integer ) info .get ("h" )).floatValue ()
18571927 / ((Integer ) info .get ("w" )).floatValue ();
18581928 }
1929+
1930+ // position the mask off the page so it can't be seen
1931+ if (isMask ) {
1932+ coords = new Coordinate (
1933+ (this .currentOrientation == Orientation .PORTRAIT ? this .fwPt : this .fhPt ) + 10 ,
1934+ coords .getY ()
1935+ );
1936+ }
1937+
18591938 this ._out (String .format (Locale .ENGLISH ,
18601939 "q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q" ,
18611940 Float .valueOf (w1 * this .k ), Float .valueOf (h1 * this .k ), Float .valueOf (coords .getX () * this .k ),
0 commit comments