Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Image File to PDF Stream Conversion

I am creating a routine to write a simple PDF document based on the information in this document from Adobe. Creating a stream for text and shapes has proven straightforward but I am stuck on inserting an image.

Could anyone provide a simple explanation of how to convert a file (any image format like gif, bmp, jpg etc. would be fine) to a PDF stream? Note that I do not want to create an entire PDF file, just a stream within the file.

With the apps I have available it is not possible to look at how it is done elsewhere because the entire stream is encoded from beginning to end and it is this encoding method that I am trying to work out.

Whilst I do not want to re-invent the entire creating a PDF file wheel, I do want to understand how this particular part of it works so do not want to use a library (thus the reason for not mentioning the language I am using).

like image 894
blankabout Avatar asked Dec 05 '11 19:12

blankabout


2 Answers

You need to use the Do operator within the content stream. E.g.

....    /Im1 Do .......

Im1 refers to an XObject resource in the page's resource dictionary

For instance,

In the page dictionary ...
<< 

...
/Contents 1 0 R
/Resources << /XObject << /Im1 2 0 R >> >>
...
>>

Object 2 0 R will be an image XObject:

2 0 obj << /Type /XObject /Subtype /Image /Width 100 /Height 100 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length 10000 /Filter /DCTDecode >>
stream
JPEG DATA HERE
endstream
endobj

A few notes: - to position and scale the image you must set the current graphics matrix using the cm operator. For example

150 0 0 150 100 100 cm

will position the image at (100,100) and make the image 150 wide and 150 high.

  • You're not limited to JPEGs - you can use JPEG2000s (use /Filter=/JPXDecode) or bitmap pixel data (omit the filter)

  • The section of the spec that has all this in is 8.9

  • I haven't experimented with LZW decode - I guess that might work for GIF

  • you typically push the graphics state onto the stack when displaying an image. e.g.

    q a b c d e f cm /Im1 Do Q

The q and Q operators push and pop the graphics state (importantly, the cm operator!)

like image 102
Jimmy Avatar answered Oct 14 '22 03:10

Jimmy


A simple C# program to create a pdf from a jpg based on the above can be found here.

Note the detail that the word "stream" and the actual jpg-stream MUST be separated by \n (or \r\n) !!!

Best Regards Eske Rahn

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Drawing;

namespace ConsoleApplication1
{
    class Program
    {
        static void WriStr(FileStream Out, string s)
        {
            Out.Write(System.Text.Encoding.ASCII.GetBytes(s), 0, s.Length);
        }
        static void Main(string[] args)
        {

            string InJpg = @"InFile.JPG";
            string OutPdf = @"OutFile.pdf";

            byte[] buffer = new byte[8192];
            var stream = File.OpenRead(InJpg); // The easiest way to get the metadata is to temporaryly load it as a BMP
            Bitmap bmp = (Bitmap)Bitmap.FromStream(stream);
            int w = bmp.Width; String wf = (w * 72 / bmp.HorizontalResolution).ToString().Replace(",", ".");
            int h = bmp.Height; ; string hf = (h * 72 / bmp.VerticalResolution).ToString().Replace(",", ".");
            stream.Close();

            FileStream Out = File.Create(OutPdf);

            var lens = new List<long>();

            WriStr(Out, "%PDF-1.5\r\n");

            lens.Add(Out.Position);
            WriStr(Out, lens.Count.ToString() + " 0 obj " + "<</Type /Catalog\r\n/Pages 2 0 R>>\r\nendobj\r\n");

            lens.Add(Out.Position);
            WriStr(Out, lens.Count.ToString() + " 0 obj " + "<</Count 1/Kids [ <<\r\n" +
                        "/Type /Page\r\n" +
                        "/Parent 2 0 R\r\n" +
                        "/MediaBox [0 0 " + wf + " " + hf + "]\r\n" +
                        "/Resources<<  /ProcSet [/PDF /ImageC]\r\n /XObject <</Im1 4 0 R >>  >>\r\n" +
                        "/Contents 3 0 R\r\n" +
                        ">>\r\n ]\r\n" +
                        ">>\r\nendobj\r\n");

            string X = "\r\n" +
                "q\r\n" +
                "" + wf + " 0 0 " + hf + " 0 0 cm\r\n" +
                "/Im1 Do\r\n" +
                "Q\r\n";
            lens.Add(Out.Position);
            WriStr(Out, lens.Count.ToString() + " 0 obj " + "<</Length " + X.Length.ToString() + ">>" +
                        "stream" + X + "endstream\r\n" +
                        "endobj\r\n");
            lens.Add(Out.Position);
            WriStr(Out, lens.Count.ToString() + " 0 obj " + "<</Name /Im1" +
                        "/Type /XObject\r\n" +
                        "/Subtype /Image\r\n" +
                        "/Width " + w.ToString() +
                        "/Height " + h.ToString() +
                        "/Length 5 0 R\r\n" +
                        "/Filter /DCTDecode\r\n" +
                        "/ColorSpace /DeviceRGB\r\n" +
                        "/BitsPerComponent 8\r\n" +
                        ">> stream\r\n");
            long Siz = Out.Position;
            var in1 = File.OpenRead(InJpg);
            while (true)
            {
                var len = in1.Read(buffer, 0, buffer.Length);
                if (len != 0) Out.Write(buffer, 0, len); else break;
            }
            in1.Close();
            Siz = Out.Position - Siz;
            WriStr(Out, "\r\nendstream\r\n" +
                        "endobj\r\n");

            lens.Add(Out.Position);
            WriStr(Out, lens.Count.ToString() + " 0 obj " + Siz.ToString() + " endobj\r\n");

            long startxref = Out.Position;

            WriStr(Out, "xref\r\n" +
                        "0 " + (lens.Count + 1).ToString() + "\r\n" +
                        "0000000000 65535 f\r\n");
            foreach (var L in lens)
                WriStr(Out, (10000000000 + L).ToString().Substring(1) + " 00000 n\r\n");
            WriStr(Out, "trailer\r\n" +
                        "<<\r\n" +
                        "  /Size " + (lens.Count + 1).ToString() + "\r\n" +
                        "  /Root 1 0 R\r\n" +
                        ">>\r\n" +
                        "startxref\r\n" +
                        startxref.ToString() + "\r\n%%EOF");
            Out.Close();
        }
    }
}

ADD 2016-04-07:

Here is a later version with comments, support for scaling and multi-JPG pages and a full program main-wrapper (the additional functionality was so easy to add, that it would be a pity to omit...)

using System;
using System.Collections.Generic;
//using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;

namespace Jpg2Pdfdir
{
    class Program
    {
        static void WriStr(FileStream Out, string s, params object[] args)
        {
            s = string.Format(s, args);
            Out.Write(System.Text.Encoding.ASCII.GetBytes(s), 0, s.Length);
        }
        //Combined from http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf

        /// <summary>
        /// Create a pdf from a list of jpgs, optionally stretching&compressing them. (Note the scaling is a display&print thing only, the jpg_stream itself is included unchanged) 
        /// </summary>
        /// <param name="InJpgs">List of Jpg (full)names</param>
        /// <param name="OutPdf">Name of the pdf to create</param>
        /// <param name="StretchWs">For each jpg the width-scaling factor, fall back to the last given, and if none to 1.0</param>
        /// <param name="StretchHs">For each jpg the height scalling, none positive or missing value is replaced with the width scale value (to keep aspect ratio)</param>
        static void JpgToPdf(List<string> InJpgs, string OutPdf, List<Double> StretchWs, List<Double> StretchHs)
        {
            if (StretchWs==null || StretchWs.Count==0)StretchWs=new List<double>{1.0}; //default to unchanged
            if (StretchHs==null)StretchHs=new List<double>{}; //Default to all with same aspect ratio

            byte[] buffer = new byte[8192];
            int[] ws = new int[InJpgs.Count];
            int[] hs = new int[InJpgs.Count];
            string[] wfs = new string[InJpgs.Count];
            string[] hfs = new string[InJpgs.Count];
            for (int i=0;i<InJpgs.Count;i++) {
                double StretchW=i<StretchWs.Count?StretchWs[i]:StretchWs[StretchWs.Count-1]; // Fall back to the last
                double StretchH=i<StretchHs.Count && 0<StretchHs[i]?StretchHs[i]:StretchW; //Fall back to same X-Y scale.
                System.IO.FileStream stream = File.OpenRead(InJpgs[i]);
                // The easiest way to get the metadata is to temporaryly load the file, ignoring the ImageData!
                using (Image Img = Image.FromStream(stream,false, false)) { //Last parameter: vaildateImageData=FALSE
                    ws[i] = Img.Width ; wfs[i] = (ws[i] * StretchW * 72 / Img.HorizontalResolution).ToString(System.Globalization.CultureInfo.InvariantCulture);
                    hs[i] = Img.Height; hfs[i] = (hs[i] * StretchH * 72 / Img.VerticalResolution  ).ToString(System.Globalization.CultureInfo.InvariantCulture);
                }
                stream.Close();
            }

            FileStream Out = File.Create(OutPdf);

            //Holds the object-positions (Or lengths before)
            var lens = new List<long>();

            //Must have header
            WriStr(Out, "%PDF-1.5\r\n");

            //Obj 1 The catalog, pointing to the pages in object 2
            lens.Add(Out.Position);
            WriStr(Out, "{0} 0 obj " + "<</Type /Catalog\r\n/Pages 2 0 R>>\r\nendobj\r\n", lens.Count);

            //Obj 2 The pageS, with inline object for the Kids object of type Page
            //Note the size in the MediaBox, The resource for the image in object 4 (Streams can not be inline objects)
            //And the Contents in object 3, and that the Parent of the Page points back to object 2 self.
            lens.Add(Out.Position);
            String Pages = "";
            for (int i = 0; i < InJpgs.Count; i++) {
                Pages+= "<<\r\n"+
                        "/Type /Page\r\n" +
                        "/Parent 2 0 R\r\n" +
                        "/MediaBox [0 0 " + wfs[i] + " " + hfs[i] + "]\r\n" +
                        "/Resources << /XObject <</Im"+(1+i).ToString()+" "+(4+3*i).ToString()+" 0 R >>  >>\r\n" +
                        "/Contents "+(3+3*i).ToString()+" 0 R\r\n" +
                        ">>\r\n";
            }
            WriStr(Out, "{0} 0 obj <</Type /Pages /Count {1} /Kids [{2}]\r\n" +
                        ">>\r\nendobj\r\n", lens.Count, InJpgs.Count, Pages);

            for (int i = 0; i < InJpgs.Count; i++) {

                // Obj 3+3i. The command stream to do the image Im# in a string, so the length can be evaluated. Note this is WITHOUT the leading and trailing CRLF 
                string X =
                    "q\r\n" +
                    "" + wfs[i] + " 0 0 " + hfs[i] + " 0 0 cm\r\n" +
                    "/Im"+(1+i).ToString()+" Do\r\n" +
                    "Q";
                lens.Add(Out.Position);
                WriStr(Out, lens.Count.ToString() + " 0 obj <</Length {0}>> stream\r\n" +
                            "{1}\r\n" +
                            "endstream\r\n" +
                            "endobj\r\n", X.Length, X);

                // Obj 4+3i of type XObject containing the jpg-stream, and with a reference to the length that will be stored in object 5 when known
                lens.Add(Out.Position);
                WriStr(Out, "{0} 0 obj <</Name /Im{1}" +
                            "/Type /XObject\r\n" +
                            "/Subtype /Image\r\n" +
                            "/Width {2}"+ 
                            "/Height {3}"+
                            "/Length {4} 0 R\r\n" +
                            "/Filter /DCTDecode\r\n" +
                            "/ColorSpace /DeviceRGB\r\n" +
                            "/BitsPerComponent 8\r\n" +
                            ">> stream\r\n", lens.Count, 1+i, ws[i], hs[i], 5+3*i);
                long Siz = Out.Position;
                var in1 = File.OpenRead(InJpgs[i]);
                while (true)
                {
                    var len = in1.Read(buffer, 0, buffer.Length);
                    if (len != 0) Out.Write(buffer, 0, len); else break;
                }
                in1.Close();
                Siz = Out.Position - Siz; // The difference is the stream-length
                WriStr(Out, "\r\nendstream\r\n" +
                            "endobj\r\n");

                // Obj 5+3i the stream length (not known at the time of the begining of object 4
                lens.Add(Out.Position);
                WriStr(Out, "{0} 0 obj {1} endobj\r\n",lens.Count ,Siz);

            }
            //Pointer for XREF-table saved
            long startxref = Out.Position;

            //The XREF table, note the zero'th object, it is the free-object-list not used here
            WriStr(Out, "xref\r\n" +
                        "0 {0}\r\n" +
                        "0000000000 65535 f\r\n", lens.Count+1);
            //Position of each object saved entered in the XREF
            foreach (var L in lens)
                WriStr(Out, (10000000000 + L).ToString().Substring(1) + " 00000 n\r\n");
            //The trailer, pointing to object 1 as the Root
            //and the saved startxref last, judt before the %%EOF marker
            WriStr(Out, "trailer\r\n" +
                        "<<\r\n" +
                        "  /Size {0}\r\n" +
                        "  /Root 1 0 R\r\n" +
                        ">>\r\n" +
                        "startxref\r\n", lens.Count+1);
            WriStr(Out, startxref.ToString() + "\r\n" +
                        "%%EOF");
            Out.Close();
        }



        static void Main(string[] args)
        {
            if (0==args.Length)  { 
                Console.WriteLine("Call with {JpgName [ScaleXY | ScaleW ScaleH] } [OutputName] , OutputName defaults to first .jpg -> .pdf");
                return;
            }
            List<string> basejpgs = new List<string>();
            double WrkDouble;
            List<double> ScaFacWs = new List<double>();
            List<double> ScaFacHs = new List<double>();
            int i = 0;
            while(i<args.Length && System.IO.File.Exists(args[i]) && System.IO.Path.GetExtension(args[i]).ToLower()==".jpg") {
                basejpgs.Add(args[i]);
                i++;
                if (i<args.Length && Double.TryParse(args[i], out WrkDouble)) {
                    i++;
                } else {
                    WrkDouble=1.0; //Default to 1x
                }
                ScaFacWs.Add(WrkDouble);
                if (i < args.Length && Double.TryParse(args[i], out WrkDouble))
                {
                    i++;
                } else {
                    WrkDouble=-1; //Default to same x-y scale
                }
                ScaFacHs.Add(WrkDouble);
            }
            //if (basejpgs.Count==0) basejpgs.Add("Red16x16.jPg"); //####DEBUG####
            string destpdf = basejpgs[0];
            if (i<args.Length && (System.IO.Path.GetExtension(args[i]).ToLower()==".pdf" || System.IO.Path.GetExtension(args[i])=="")) { 
                destpdf=args[i];
                i++;
            }
            if (i<args.Length)  { 
                Console.WriteLine("Too many arguments, or could not decode???");
            }
            destpdf = System.IO.Path.ChangeExtension(destpdf, ".PDF");
            JpgToPdf(basejpgs, destpdf, ScaFacWs, ScaFacHs);
        }
    }
}
like image 32
Eske Rahn Avatar answered Oct 14 '22 01:10

Eske Rahn