C#でPDFファイルのHello World (ライブラリ不使用)

# 目的

pdfのフォーマットが[公式の資料](https://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=2ahUKEwicxcnatO3lAhX3xosBHXQxBWAQFjAAegQIBBAC&url=https%3A%2F%2Fwww.adobe.com%2Fcontent%2Fdam%2Facom%2Fen%2Fdevnet%2Fpdf%2Fpdfs%2FPDF32000_2008.pdf&usg=AOvVaw1-RSI9319qZLGt7YE-l-gc)だけだと読み解けないところがあるので、PDFファイルを作って Adobe Reader でどうなるか見てみたいと思った。
ので、PDFファイルのリファレンステーブルとかめんどうな部分の生成を補助するコードを書いてみた。

# 参考サイト

indirect objectの中身はほぼこちらを参考にさせていただきました。
https://itchyny.hatenablog.com/entry/2015/09/16/100000

# サンプルPDF

※改行コードは \n (Lf) (0x0A) としてASCIIで保存してください。byte位置のテーブル情報を含んでいるため、byteがずれると正常なPDFになりません。念のため`%%EOF`の末尾にも改行コードをいれてください。
(今回はASCII文字しか含んでいないので、UTF-8またはShift_JISでも可)

```PDF

%PDF-1.4
1 0 obj<</Type/Catalog /Pages 2 0 R>>
endobj
2 0 obj<</Type/Pages /Kids [4 0 R] /Count 1>>
endobj
3 0 obj<</Length 58>>stream
1. 0. 0. 1. 50. 720. cm BT /F0 36 Tf (Hello, world!) Tj ET
endstream
endobj
4 0 obj<</Type/Page /Parent 2 0 R /Resources 5 0 R /MediaBox [0 0 595 842] /Contents 3 0 R>>
endobj
5 0 obj<</Font <</F0 <</Type /Font /BaseFont /Times-Roman /Subtype /Type1 >> >> >>
endobj
xref
0 6
0000000000 65535 f 
0000000009 00000 n 
0000000054 00000 n 
0000000107 00000 n 
0000000211 00000 n 
0000000311 00000 n 
trailer
<</Size 6 /Root 1 0 R>>
startxref
401
%%EOF

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/79ace056-5a67-60d1-85cd-11e25dbf506e.png)


# 生成用ソースコード

```csharp

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

class MyPdfGenerator
{
    class IndirectObjInfo
    {
        public int objId;
        public int genId;
        public int pos;

        public IndirectObjInfo(int oId, int gId, int p)
        {
            objId = oId;
            genId = gId;
            pos = p;
        }
    }

    Stream _st;
    Dictionary<int,IndirectObjInfo> _indirectObjs;
    int _largestObjId;
    int _numberOfObjId; // objId = 0 を除いた個数
    int _xrefPos;

    public MyPdfGenerator(Stream st)
    {
        _st = st;
        _indirectObjs = new Dictionary<int,IndirectObjInfo>();
        _largestObjId = -1;
        _numberOfObjId = 0;
        _xrefPos = -1;
    }

    void WriteStringToStream(string s)
    {
        byte[] buf = Encoding.ASCII.GetBytes(s);
        _st.Write(buf, 0, buf.Length);
    }

    public void WriteHeader()
    {
        if (_st.Position!=0){throw new Exception("Position error.");}
        WriteStringToStream("%PDF-1.4\n");
        /*
        WriteStringToStream("%");
        byte[] tmp = new byte[]{0xe2,0xe3,0xcf,0xd3};
        _st.Write(tmp,0,tmp.Length);
        WriteStringToStream("\n");
        */
    }


    public void WriteIndirectObj(int objId, string dictPart)
    {
        WriteIndirectObj(objId, dictPart, null);
    }

    public void WriteIndirectStreamObj(int objId, string streamData)
    {
        WriteIndirectStreamObj(objId, Encoding.ASCII.GetBytes(streamData));
    }

    public void WriteIndirectStreamObj(int objId, byte[] streamData)
    {
        string dictPart = "<</Length "+ streamData.Length.ToString() +">>";
        WriteIndirectObj(objId, dictPart, streamData);
    }

    void WriteIndirectObj(int objId, string dictPart, byte[] streamData)
    {
        if (objId<=0) {throw new Exception("ObjId must be non-zero positive number");}

        int pos = (int)_st.Position;
        StringBuilder sb = new StringBuilder(20+dictPart.Length);
        sb.Append(objId.ToString());
        sb.Append(" 0 obj");
        sb.Append(dictPart);
        WriteStringToStream(sb.ToString());
        sb.Clear();
        if (streamData != null) {
            WriteStringToStream("stream\n");
            _st.Write(streamData, 0, streamData.Length);
            WriteStringToStream("\nendstream");
        }
        sb.Append("\n");
        sb.Append("endobj\n");
        WriteStringToStream(sb.ToString());

        if (_largestObjId < objId) {
            _largestObjId = objId;
        }
        _indirectObjs.Add(objId, new IndirectObjInfo(objId,0,pos)); // Addメソッドを使用することで、キーが重複すると例外を投げてくれる
        _numberOfObjId++;
    }

    

    public void WriteXref()
    {
        if ( _numberOfObjId != _largestObjId ) {
            throw new Exception("Failed.");
        }
        _xrefPos = (int)_st.Position;

        StringBuilder sb = new StringBuilder(20+(_numberOfObjId+1)*20);
        sb.Append("xref\n");
        sb.Append("0 "); sb.Append((_numberOfObjId+1).ToString()); sb.Append("\n");
        sb.Append("0000000000 65535 f \n");
        for ( int i=1 ; i <= _largestObjId ; i++) {
            sb.Append(_indirectObjs[i].pos.ToString("D10"));
            sb.Append(" 00000 n \n");
        }

        WriteStringToStream(sb.ToString());
    }

    public void WriteTrailer(int rootObjId)
    {
        StringBuilder sb = new StringBuilder(50);
        sb.Append("trailer\n<<");
        sb.Append("/Size " + (_numberOfObjId+1).ToString() + " ");
        sb.Append("/Root " + rootObjId.ToString()+" 0 R");
        sb.Append(">>\n");
        WriteStringToStream(sb.ToString());
    }

    public void WriteStartXref()
    {
        StringBuilder sb = new StringBuilder(20);
        sb.Append("startxref\n");
        sb.Append(_xrefPos.ToString());
        sb.Append("\n");
        WriteStringToStream(sb.ToString());
    }

    public void WriteEof()
    {
        WriteStringToStream("%%EOF\n");
    }
}

class PdfGenTest
{
    [STAThread]
    static void Main(string[] args)
    {
        var st = new FileStream("testout.pdf", FileMode.Create); // over write
        var pg = new MyPdfGenerator(st);

        pg.WriteHeader();
        pg.WriteIndirectObj(1,"<</Type/Catalog /Pages 2 0 R>>"); // [1] Root Object
        pg.WriteIndirectObj(2,"<</Type/Pages /Kids [4 0 R] /Count 1>>"); // [2] Pages Object
        pg.WriteIndirectStreamObj(3, @"1. 0. 0. 1. 50. 720. cm BT /F0 36 Tf (Hello, world!) Tj ET"); // [3] Contents
        pg.WriteIndirectObj(4,"<</Type/Page /Parent 2 0 R /Resources 5 0 R /MediaBox [0 0 595 842] /Contents 3 0 R>>"); // [4] Page Object
        pg.WriteIndirectObj(5,"<</Font <</F0 <</Type /Font /BaseFont /Times-Roman /Subtype /Type1 >> >> >>"); // [5] Resources

        pg.WriteXref();
        pg.WriteTrailer(1);
        pg.WriteStartXref();
        pg.WriteEof();
    }
}
```