Repository: python-pillow/Pillow

Issue:
PDFs and 1 bit pixels
I posted this question [on Stack Overflow](http://stackoverflow.com/questions/36121914/pillow-pdf-and-1-bit-pixels) before realizing that perhaps I should post it here.

I'm trying to create a PDF of a complicated black-and-white rectangular grid, nominally 7 inches wide by 10 inches tall.  I'm using Pillow to do the conversion.  If I specify a PNG, everything works fine (that is, the aspect ratio is 7 by 10).  If I specify a PDF, the output is 200 inches wide and 0.04 inches tall.  What's going on?

Incidentally, if I set `mode='RGB'`, then the output PDF dimensions are correct, but Pillow performs a JPEG encoding of the image. JPEG converts my sharp black and white grid into smoother shades of gray (which is anathema for my application).

```python
import PIL
from PIL import Image

# Let's consider a 7" x 10" rectangle of pixels
inches_wide=7
inches_tall=10
dpi=200
num_pixel_rows=inches_tall*dpi
num_pixel_cols=inches_wide*dpi

# Make an image
mode='1'
img = Image.new( mode, (num_pixel_cols,num_pixel_rows))
img.info['dpi'] = (dpi,dpi)

# Saving it as PNG, we get a black rectangle, as expected.
img.save('big_rectangle.png','png',resolution=dpi)
# Saving it as a PDF, we get a 200" x 0.04" all-white line?!
img.save('big_rectangle.pdf','pdf',resolution=dpi)

# Versioning info:
print 'PILLOW',PIL.PILLOW_VERSION
print 'PIL',PIL.VERSION
#  My output:
#  PILLOW 3.1.1
#  PIL 1.1.7
```



Discussion:
Not sure what's going on… should we call this a bug?
So I investigated this a bit and it is a bug that existed pre-fork as best I can tell. The lines in question are in PdfImagePlugin.py(line:173):

```python
if filter == "/ASCIIHexDecode":
    if bits == 1:
        # FIXME: the hex encoder doesn't support packed 1-bit
        # images; do things the hard way...
        data = im.tobytes("raw", "1")
        im = Image.new("L", (len(data), 1), None)
        im.putdata(data)
    ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)])
```

The problem lies in the `im = Image.new()` call. The size is specified as `(len(data),1)` which is `(350000, 1)` in the example code. It works to change it to `im.size`, but that has no compression.

I would assume anyone using mode 1 is probably wanting to keep their output file super small. The ASCIIHex filter has no 'image' compression or data compression and generates a 5.5 MB file in the example. The equivalent PNG is 421 Bytes. The ideal situation would be for someone to implement one of the lossless algorithms and supply a flag to the `save()` function to use DCT or lossless encoding. Options are:

1. Change to `im.size` and convert to mode "L" as now
2. Convert to mode "P" further upstream (uses ASCIIHex instead of DCT)
3. Generate some sort error on mode "1", forcing user to convert to P
#3827 has been merged, so the dimensions problem should now be fixed. However, there seems to also be a request for compression here.

Resolving patch:
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -124,7 +124,7 @@ def _save(im, fp, filename, save_all=False):
             decode = None
 
             if im.mode == "1":
-                filter = "ASCIIHexDecode"
+                filter = "DCTDecode"
                 colorspace = PdfParser.PdfName("DeviceGray")
                 procset = "ImageB"  # grayscale
                 bits = 1
@@ -161,12 +161,6 @@ def _save(im, fp, filename, save_all=False):
             op = io.BytesIO()
 
             if filter == "ASCIIHexDecode":
-                if bits == 1:
-                    # FIXME: the hex encoder doesn't support packed 1-bit
-                    # images; do things the hard way...
-                    data = im.tobytes("raw", "1")
-                    im = Image.new("L", im.size)
-                    im.putdata(data)
                 ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
             elif filter == "DCTDecode":
                 Image.SAVE["JPEG"](im, op, filename)


Test patch:
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -42,7 +42,8 @@ def test_monochrome(tmp_path):
     mode = "1"
 
     # Act / Assert
-    helper_save_as_pdf(tmp_path, mode)
+    outfile = helper_save_as_pdf(tmp_path, mode)
+    assert os.path.getsize(outfile) < 15000
 
 
 def test_greyscale(tmp_path):