Skip to content

Commit 0e61738

Browse files
committed
Merge pull request #2 from austinhyde/gif-support
GIF support, Alpha-channel PNG support
2 parents 4d028a6 + cd595ce commit 0e61738

File tree

2 files changed

+114
-33
lines changed

2 files changed

+114
-33
lines changed

src/main/java/com/koadweb/javafpdf/FPDF.java

Lines changed: 111 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,27 @@
2121
*/
2222
package com.koadweb.javafpdf;
2323

24+
import com.koadweb.javafpdf.util.Compressor;
2425
import java.awt.color.ColorSpace;
2526
import java.awt.image.BufferedImage;
27+
import java.io.ByteArrayInputStream;
28+
import java.io.ByteArrayOutputStream;
2629
import java.io.File;
27-
import java.io.FileInputStream;
2830
import java.io.FileOutputStream;
2931
import java.io.IOException;
3032
import java.io.InputStream;
3133
import java.io.OutputStream;
3234
import java.io.UnsupportedEncodingException;
35+
import java.nio.file.Files;
3336
import java.util.ArrayList;
3437
import java.util.Calendar;
3538
import java.util.HashMap;
3639
import java.util.List;
3740
import java.util.Locale;
3841
import java.util.Map;
3942
import java.util.Set;
40-
4143
import 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),

src/main/java/com/koadweb/javafpdf/ImageType.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,7 @@ public enum ImageType {
3232
/** Portable Network Graphics image. */
3333
PNG,
3434
/** Joint Photographic Experts Group image. */
35-
JPEG;
35+
JPEG,
36+
/** Graphics Interchange Format image. */
37+
GIF;
3638
}

0 commit comments

Comments
 (0)