challenge Hello, world! PDF版

Hello, world!シリーズの続編です。 「Hello, world!」となるべく大きく書かれた1ページのPDFを出力してください。

Posted feedbacks - Nested

Flatten Hidden

とりあえず。iTextで。

1
2
3
4
5
6
7
8
9
import com.lowagie.text._
import com.lowagie.text.pdf.PdfWriter
import java.io.FileOutputStream

val document = new Document(PageSize.A4, 50, 50, 50, 50);
val writer = PdfWriter.getInstance(document, new FileOutputStream("HelloWorld.pdf"));
document.open
document.add(new Paragraph("Hello, world!", FontFactory.getFont(FontFactory.COURIER, 60, Font.BOLD)));
document.close
うわぁ、一番乗りそこねたぁ
1
2
3
4
5
6
require 'rubygems'
require 'pdf/writer'
PDF::Writer.new do |pdf|
  pdf.text "Hello, world!", :font_size => 150, :justification => true
  pdf.save_as "50.pdf"
end
 他力本願。

 'http://ja.doukaku.org/50' のところを location.href に置き換えると,カレントページのPDF版を取得するブックマークレットになります。
1
javascript:location.href='http://www.html2pdf.biz/api?url='+'http://ja.doukaku.org/50'+'&ret=PDF'

試してみましたが、少なくとも僕の環境では1ページに収まってHello, world!が含まれているので題意は満たしていると思います。

>「Hello, world!」となるべく大きく書かれた
と要件にはあります。
このページをただ出力することが「なるべく大きく」書こうと作っているようには思えませんでした。(実際に大きいかどうかは別として)

おっしゃりたいことはわかりますが 「なるべく大きくしようとしたかどうか」は 客観的にははかることができません。 もしお題に「A4の紙に印刷した場合に10cm以上であること」などと書いてあれば 客観的に判断できる「要件」だと思いますが 「なるべく大きく」が要件だというのには違和感を感じます。

ブラウザ上のJavaScriptではそもそもHello, worldをPDFで出力すること自体が かなり困難なのだと思います。 それを乗り越えたshimakumaさんの「Webサービスをライブラリのように使う」という発想は面白いと思いました。こういうコードを許容するためにもあまり要件を厳しく設定したくはないです。

誰かがJavaScriptでもっと大きく出力するコードを投稿したら、僕はそちらにプラスをつけてshimakumaさんのコードは0に戻すかも知れません。そちらの方が「このコードは要件を満たしていない」と書くよりも生産的だと思います。

私が狭量だったようです。
技術的な話もありませんし不快に思っている方も多いようなので私の発言は消していただければ幸いです。
HPDFというライブラリを利用する
A4サイズにななめに入れてみました
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import Graphics.PDF
import Data.Monoid

main = let document =  rgbSpace
                    <> chooseFont Helvetica 57
                    <> applyMatrix (rotate (Degree 58))
                    <> fillText 25 (-28) "Hello, world!"
                    <> emptyPdf 210 297
       in
       writePdf "hello.pdf" document 
あ、なるほど、確かに対角線に入れるのが一番大きいですね。
XSL-FO です。
Apache FOP 等で
fop -c fop.xconf hello.fo hello.pdf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8" ?>
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master
    page-height="210mm" page-width="297mm"
    margin-top="20mm" margin-bottom="20mm"
    margin-left="20mm" margin-right="20mm"
    master-name="Pagemaster-1" >
      <fo:region-body
      margin-top="5mm" margin-bottom="5mm"
      margin-left="5mm" margin-right="5mm" />
      <fo:region-before extent="10mm" />
      <fo:region-after extent="10mm" />
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="Pagemaster-1" >
    <fo:flow flow-name="xsl-region-body">
      <fo:block font-size="96pt" text-align="center">
        Hello, world!
      </fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

	
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

cat << EOS > foo
\documentclass{article}
\usepackage{type1cm}
\begin{document}
{\fontsize{200pt}{220pt}\selectfont Hello World}
\end{document}
EOS

latex foo
dvipdfm foo
tag に TeX 入れるべきだったかな?
文字列の幅を取得する関数が、多分あるんだろうけど、見つけられなかったので
ダミーの書き込みにより取得しているところが、ちょっとばかり冗漫ですね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from reportlab.pdfgen import canvas

c = canvas.Canvas('hello.pdf')
t = c.beginText()
t.setFont(t._fontname, 64)
t.setTextOrigin(0, 0)
t.textOut('Hello, World')
h = 64 / t.getX() * c._pagesize[1]
t.setFont(t._fontname, h)
c.setFontSize(h)
c.rotate(-90)
c.drawString(-c._pagesize[1], t._leading - h, 'Hello, World')
c.save()
他の人のコードを見たら、サイズ決め打ちでやってるようなので
同等の必要最小限版を投稿しておきます。
1
2
3
4
5
6
7
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4

c = canvas.Canvas('hello.pdf', A4[::-1])
c.setFontSize(158.4)
c.drawString(0, 31.52, 'Hello, World')
c.save()
連続投稿で失礼します。

#2480は、まだ無駄なコードが多かったので、ブラッシュアップ版を
投稿させてください。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from reportlab.pdfgen import canvas

c = canvas.Canvas('hello.pdf')
t = c.beginText()
t.setFont(t._fontname, 64)
t.textOut('Hello, World')
c.setFont(c._fontname, 64 / t.getX() * c._pagesize[1])
c.rotate(-90)
c.drawString(-c._pagesize[1], c._leading - c._fontsize, 'Hello, World')
c.save()
Tk キャンバスの内容を PDF に出力する Trampoline! というライブラリを使用してみました。
1
2
3
4
5
6
7
package require trampoline

. configure -menu [menu .menu]
.menu add command -label {Save as PDF} -command {::pdf::generate .c hello.pdf}

pack [canvas .c -width 297 -height 210  -bg white]
.c create text 150 105 -text "Hello,\nWorld!" -font {Helvetica 70} -justify center
SPDF をインストールした Squeak Smalltalk で。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
| file pdfWriter page text textState textPositioning |
file := FileStream fileNamed: 'doukaku50.pdf'.
pdfWriter := PDFWriter on: file.
pdfWriter compressionOff.
page := pdfWriter defaultPage.
text := PDFTextObject for: page pageDescription.
textState := PDFTextState for: text.
textState fontPitch: 140.
textState font: (PDFFont type1Helvetica).
textPositioning := PDFTextPositioning for: text.
textPositioning coordinate: (PDFPoint x: 10 y: 250).
text addOperator: textPositioning.
text addOperator: textState.
text write: 'Hello, world!'.
page addTextObject: text.
pdfWriter close.
file close
iText を使用しています。なるべく大きくという事で、A0 横の用紙に横幅いっぱいに描画してみました(別に A0 にする必要はなかったかも知れません)。折り返してさらに大きく書く事もできたのですが、横一行のほうが題意に合っていると看做しました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.FileOutputStream;
import com.lowagie.text.Document;
import com.lowagie.text.Paragraph;
import com.lowagie.text.PageSize;
import com.lowagie.text.Font;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfWriter;

public class Sample {
    static final String TEXT = "Hello, World!";
    public static void main(String[] args) throws Exception {
        Document doc = new Document(PageSize.A0.rotate());
        PdfWriter.getInstance(doc, new FileOutputStream("HelloWorld.pdf"));
        doc.open();
        BaseFont bf = BaseFont.createFont(BaseFont.TIMES_ROMAN, 
                                      "US-ASCII", false);
        float s = (doc.right() - doc.left() - doc.rightMargin()) /
            bf.getWidthPoint(TEXT, 1.0f);
        Font f = new Font(Font.TIMES_ROMAN, s);
        doc.add(new Paragraph(TEXT, f));
        doc.close();
    }
}
いくつか修正します。
・用紙サイズを A0 から B0 に変更しました(どうでも良いですね……)
・描画サイズのマージンを取りすぎていたので、ぎりぎりまで大きくしました。
・FontをBaseFontから直接生成するようにしました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
diff 2487.java Sample.java
12c12
<       Document doc = new Document(PageSize.A0.rotate());
---
>       Document doc = new Document(PageSize.B0.rotate());
17,19c17,18
<       float s = (doc.right() - doc.left() - doc.rightMargin()) /
<           bf.getWidthPoint(TEXT, 1.0f);
<       Font f = new Font(Font.TIMES_ROMAN, s);
---
>       float size = (doc.right() - doc.left()) / bf.getWidthPoint(TEXT, 1.0f);
>       Font f = new Font(bf, size);
xycairo (xyzzy の cairo バインディング) を使って書きました。

cl-cairo2 を使えばもう少しシンプルに書けると思います
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(require :cairo)
(use-package :cairo)
(use-package :cairo.ext)

(defun hello-pdf (width height text)
  (flet ((calc-font-size (cr x text)
           (with-cairo-save (cr)
             (cairo-set-font-size cr 100.0)
             (* 100.0 (/ x (cdr (assoc :width (cairo-text-extents-alist cr text))))))))
    (with-cairo-surface (surface (cairo-pdf-surface-create "hello.pdf" width height))
      (with-cairo (cr (cairo-create surface))
        (cairo-set-source-rgb cr 0 0 0) ; black
        (cairo-set-font-size cr (calc-font-size cr width text))
        (cairo-move-to cr 0 (/ height 2))
        (cairo-show-text cr text)))))

(hello-pdf 291 210 "Hello, world!")
#2489 の rcairo 版。

ページいっぱいにテキストを出力するために text extents から font size を計算していたが、
http://mmon.sourceforge.jp/memo/memo.html
を見ると width より x_advance を使ったほうがよさそうなのでそうしました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
require 'cairo'

width, height = 291, 210
text = "Hello, world!"

Cairo::PDFSurface.new("hello.pdf", width, height) do |surface|
  cr = Cairo::Context.new(surface)
  font_size = cr.save {
    cr.set_font_size(100.0)
    width / cr.text_extents(text).x_advance * 100.0
  }

  cr.set_font_size(font_size)
  cr.move_to(0, height / 2)
  cr.show_text(text)
  cr.show_page
end
FPDFを利用。
横長のA4で出力します。
1
2
3
4
5
6
7
8
9
<?php
require('fpdf/fpdf.php');

$pdf = new FPDF('L');
$pdf->AddPage();
$pdf->SetFont('Arial', '', 154);
$pdf->Text(0, 50, "Hello, world!");
$pdf->Output();
?>
shだったりps{2,to}pdfだったり使ってるのでちとアレですが、A4いっぱいのこんにちはなのでご容赦ください。。。

gs付属のps2pdfと、Mac OS Xのpstopdfで動作確認しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/sh
large_hello() {
  cat <<'EOHELLO'
%!
/inch {72 mul} bind def
/sheetheight 11.64 inch def
/sheetwidth   8.27 inch def

/hello     (Hello, world!) def
/hellolen  hello length    def
/fontsz    10 def
/Helvetica fontsz selectfont
/capheightratio 0.8 def

0 0 moveto
sheetwidth  hello stringwidth pop div
sheetheight fontsz capheightratio mul div
scale
hello show

showpage
EOHELLO
}
large_hello | {
  outfile=large-hello.pdf
  if type -p pstopdf >/dev/null; then
    pstopdf /dev/stdin -o $outfile
  elif type -p ps2pdf >/dev/null; then
    ps2pdf /dev/stdin $outfile
  else
    echo 'converter not found...'
    exit 1
  fi
}
決め打ちです
1
2
3
4
5
6
7
pdf("helloworld.pdf", width=8.27, height=11.69)
plot.new()
oldpar <- par(no.readonly = TRUE)
par(plt=c(0,1,0,1), mai=c(0,0,0,0), omi=c(0,0,0,0), adj=0, cex=9.2)
text(-0.178, 0.5, "Hello, world!")
par(oldpar)
dev.off()
plot.new()してからpar()してた。どおりで余白が反映されないと思った・・・。
すみません、ほとんど変化ないですが直します。
1
2
3
4
5
6
7
pdf("helloworld.pdf", width=8.27, height=11.69)
oldpar <- par(no.readonly = TRUE)
par(plt=c(0,1,0,1), mai=c(0,0,0,0), omi=c(0,0,0,0), adj=0, cex=9.2)
plot.new()
text(-0.05, 0.5, "Hello, world!")
par(oldpar)
dev.off()
panda(http://www.stillhq.com/panda/)を使いました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <panda/functions.h>
#include <panda/constants.h>

int main(int argc, char *argv[])
{
    panda_pdf *pdf;
    panda_page *page;
    panda_init();
    pdf = panda_open ("hello.pdf", "w");
    if(!pdf) panda_error(panda_true, "error");
    page = panda_newpage(pdf, panda_pagesize_a4);
    panda_setfontsize(pdf, 72);
    panda_textbox(pdf, page, 10, 10, 100, 500, "Hello World!");
    panda_close(pdf);
    return EXIT_SUCCESS;
}
OpenOffice の COM インターフェイスを使って PDF にしてみました。

フォントサイズは決めうちです。
マクロで記録した Oo Basic をそのまま直訳した感じです。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
require "win32ole"

class Hash
  def to_prop(manager)
    inject([]){|acc,pair|
      opt = manager.Bridge_GetStruct("com.sun.star.beans.PropertyValue")
      opt["Name"], opt["Value"] = *pair
      acc << opt
    }
  end
end

manager = WIN32OLE.new("com.sun.star.ServiceManager")
desktop = manager.createInstance("com.sun.star.frame.Desktop")
document = desktop.loadComponentFromURL("private:factory/swriter", "_blank", 0, [])

frame = document.getCurrentController().getFrame()
dispatcher = manager.createInstance("com.sun.star.frame.DispatchHelper")

dispatcher.executeDispatch(frame, ".uno:FontHeight", "", 0, {
  "FontHeight.Height" => 80,
  "FontHeight.Prop" => 100,
  "FontHeight.Diff" => 0,
}.to_prop(manager))

text = document.GetText
cursor = text.createTextCursor
text.insertString(cursor, "Hello, world!", 0)

dispatcher.executeDispatch(frame, ".uno:ExportDirectToPDF", "", 0, {
  "URL" => "file:///#{File.expand_path("hello.pdf")}" ,
  "FilterName" => "writer_pdf_Export",
  "SelectionOnly" => "true",
}.to_prop(manager))

document.close(false)
PDF::API2を使うのは初めて。フォントサイズが決めうちなのでよろしくなさげ。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use PDF::API2;

my $pdf = PDF::API2->new(-file => 'hello.pdf');
my $fnt = $pdf->corefont('Times-Roman');
my $page = $pdf->page;
$page->mediabox('A4');
my $gfx = $page->gfx();
$gfx->rotate(90);
$gfx->translate( 0, -120 );
$gfx->textlabel( 0, 0, $fnt, 160, 'Hello, world!' );

$pdf->save;
A4ページ全体に広げて。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
class Program {
    static void Main(string[] args) {
        string s = "Hello, world!";
        Document doc = new Document(PageSize.A4.Rotate(), 0f, 0f, 0f, 0f);
        PdfWriter.GetInstance(doc, File.Create("hello.pdf"));
        doc.Open();
        Font f = FontFactory.GetFont(FontFactory.TIMES_BOLD);
        f.Size *= doc.PageSize.Height / 
            (f.BaseFont.GetAscentPoint(s, f.Size) - f.BaseFont.GetDescentPoint(s, f.Size));
        Chunk chunk = new Chunk(s, f);
        chunk.SetTextRise(-f.BaseFont.GetAscentPoint(chunk.Content, f.Size));
        chunk.SetHorizontalScaling((doc.PageSize.Width - 0f) / chunk.GetWidthPoint());
        doc.Add(chunk);
        doc.Close();
    }
}
Gaucheだとkenhysさん作のlibharuがあるので、それを使えばPDF出力ができるのですが、
外部ライブラリを使用すると他言語の解答と似たものとなってつまらないので、自力でPDFを作成してみました。
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
(use srfi-1)
(use gauche.sequence)
(use util.list)

(define-method pdf-format ((port <port>) dict obj)
  (format port "~a " obj))

(define-method pdf-format ((port <port>) dict (kw <keyword>))
  (format port "/~a " kw))

(define-method pdf-format ((port <port>) dict (sym <symbol>))
  (cond
   ((assq sym dict) => (compose (cut format port "~d 0 R " <>) cdr))
   (else
    (format port "~a " sym))))

(define-method pdf-format ((port <port>) dict (str <string>))
  (format port "(~a) " str))

(define-method pdf-format ((port <port>) dict (vec <vector>))
  (format port "[ ")
  (for-each (cut pdf-format port dict <>) vec)
  (format port "] "))

(define-method pdf-format ((port <port>) dict (lst <list>))
  (cond
   ((null? lst) #f)
   ((is-a? (car lst) <pair>)
    (format port "<< ")
    (for-each (lambda (pair)
                (pdf-format port dict (car pair))
                (pdf-format port dict (cdr pair)))
              lst)
    (format port ">> "))
   (else
    (for-each (cut pdf-format port dict <>) lst))))

(define-method pdf-format ((port <port>) dict (proc <procedure>))
  (proc port dict))

(define (pdf-stream cmd-list)
  (lambda (port dict)
    (let ((stream (call-with-output-string
                    (lambda (out)
                      (format out "~%BT~%")
                      (for-each (lambda (cmd)
                                  (pdf-format out dict cmd)
                                  (newline out))
                                cmd-list)
                      (format out "ET~%")))))
      (pdf-format port dict `((:Length ,(string-length stream))))
      (format port "~%stream~aendstream~%" stream))))

(define (pdf-object name content)
  (lambda (port dict ctxt)
    (let ((pos (port-tell port)))
      (format port "~d 0 obj~%" (assq-ref dict name))
      (pdf-format port dict content)
      (format port "~%endobj~%")
      (acons name pos ctxt))))

(define (pdf-trailer root)
  (lambda (port dict ctxt)
    (let ((xref-pos (port-tell port)))
      (format port "xref~%0 ~d~%0000000000 65535 f~%" (+ (length dict) 1))
      (for-each (cut format port "~10,'0d 00000 n~%" <>) (map cdr (reverse ctxt)))
      (format port "trailer~%")
      (pdf-format port dict `((:Root . ,root) (:Size ,(+ (length dict) 1))))
      (format port "~%startxref~%~d~%%%EOF~%" xref-pos))))

(define (pdf-write filename root dict pdf-obj-list)
  (call-with-output-file filename
    (lambda (port)
      (format port "%PDF-1.2~%")
      (fold (cut <> port dict <>) '()
            (append pdf-obj-list (list (pdf-trailer root)))))))

(define-syntax pdf-document
  (syntax-rules ()
    ((_ filename root (name content) ...)
     (pdf-write filename 'root
                (map cons (list 'name ...) (iota (length (list 'name ...)) 1))
                (list (pdf-object 'name content) ...)))))

(pdf-document "hello.pdf" root
              (page      '((:Type . :Page)
                           (:Parent . pages)
                           (:Resources . res)
                           (:Contents . contents)))
              (pages     '((:Type . :Pages)
                           (:Kids . #(page))
                           (:Count . 1)
                           (:MediaBox . #(0 0 842 595))))
              (res       '((:ProcSet . #(:PDF :Text))
                           (:Font . ((:F1 . font)))))
              (contents   (pdf-stream '((:F1 120 Tf)
                                        (1 0 0 1 72 260 Tm)
                                        ("Hello, world!" Tj))))
              (font      '((:Type . :Font)
                           (:Subtype . :TrueType)
                           (:BaseFont . :Helvetica)))
              (root      '((:Type . :Catalog)
                           (:Pages . pages))))