diff --git a/cmd/qrbill-api/api.go b/cmd/qrbill-api/api.go index 02d411d..d335ee2 100644 --- a/cmd/qrbill-api/api.go +++ b/cmd/qrbill-api/api.go @@ -103,11 +103,12 @@ func logic() error { if format != "png" && format != "svg" && + format != "pdf" && format != "txt" && format != "html" && format != "wv" && format != "eps" { - msg := fmt.Sprintf("format (%q) must be one of png, svg, txt or html", format) + msg := fmt.Sprintf("format (%q) must be one of png, svg, pdf, eps, txt or html", format) log.Printf("%s %s", prefix, msg) http.Error(w, msg, http.StatusBadRequest) return @@ -158,6 +159,17 @@ func logic() error { w.Header().Add("Content-Type", "image/svg+xml") + case "pdf": + var err error + b, err = bill.EncodeToPDF() + if err != nil { + log.Printf("%s %s", prefix, err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/pdf") + case "eps": var err error b, err = bill.EncodeToEPS() diff --git a/internal/pdf/pdf.go b/internal/pdf/pdf.go new file mode 100644 index 0000000..570e4fa --- /dev/null +++ b/internal/pdf/pdf.go @@ -0,0 +1,334 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package pdf implements a minimal PDF 1.7 writer, just functional enough to +// create a PDF file containing a QR code encoded as rectangles. +// +// It follows the standard “PDF 32000-1:2008 PDF 1.7”: +// https://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/PDF32000_2008.pdf +package pdf + +import ( + "fmt" + "io" + "strings" + "time" +) + +func dateString(t time.Time) string { + return "D:" + t.Format("20060102150405-07'00'") +} + +// ObjectID is a PDF object id. +type ObjectID int + +// String implements fmt.Stringer. +func (o ObjectID) String() string { + return fmt.Sprintf("%d 0 R", int(o)) +} + +// Object is implemented by all PDF objects. +type Object interface { + // Objects returns all Objects which should be encoded into the + // PDF file. + Objects() []Object + + // Encode encodes the object into the PDF file w. + Encode(w io.Writer, ids map[string]ObjectID) error + + // SetID updates the object id. + SetID(id ObjectID) + + // Name returns the human-readable object name. + Name() string + + fmt.Stringer +} + +// Common represents a PDF object. +type Common struct { + ObjectName string + ID ObjectID + Stream []byte +} + +// String implements fmt.Stringer. +func (c *Common) String() string { + return c.ID.String() +} + +// SetID implements Object. +func (c *Common) SetID(id ObjectID) { + c.ID = id +} + +// Name implements Object. +func (c *Common) Name() string { + return c.ObjectName +} + +// Objects implements Object. +func (c *Common) Objects() []Object { + return []Object{c} +} + +// Encode implements Object. +func (c *Common) Encode(w io.Writer, ids map[string]ObjectID) error { + _, err := fmt.Fprintf(w, ` +%d 0 obj +<< + /Length %d +>> +stream +%s +endstream +endobj`, c.ID, len(c.Stream), c.Stream) + return err +} + +// DocumentInfo represents a PDF document information object. +type DocumentInfo struct { + Common + + CreationDate time.Time + Producer string + Title string +} + +// Objects implements Object. +func (d *DocumentInfo) Objects() []Object { + return []Object{d} +} + +// Encode implements Object. +func (d *DocumentInfo) Encode(w io.Writer, ids map[string]ObjectID) error { + _, err := fmt.Fprintf(w, ` +%d 0 obj +<< + /Title (%s) + /CreationDate (%s) + /ModDate (%s) + /Producer (%s) +>> +endobj`, + int(d.ID), + d.Title, + dateString(d.CreationDate), + dateString(d.CreationDate), + d.Producer) + return err +} + +// Catalog represents a PDF catalog object. +type Catalog struct { + Common + Pages Object // Pages +} + +// Objects implements Object. +func (r *Catalog) Objects() []Object { + return append([]Object{r}, r.Pages.Objects()...) +} + +// Encode implements Object. +func (r *Catalog) Encode(w io.Writer, ids map[string]ObjectID) error { + _, err := fmt.Fprintf(w, ` +%d 0 obj +<< + /Type /Catalog + /Pages %v +>> +endobj`, int(r.ID), r.Pages) + return err +} + +// Pages represents a PDF pages object +type Pages struct { + Common + Kids []Object // Page +} + +// Objects implements Object. +func (p *Pages) Objects() []Object { + result := []Object{p} + for _, o := range p.Kids { + result = append(result, o.Objects()...) + } + return result +} + +// Encode implements Object. +func (p *Pages) Encode(w io.Writer, ids map[string]ObjectID) error { + _, err := fmt.Fprintf(w, ` +%d 0 obj +<< + /Kids %v + /Type /Pages + /Count %d +>> +endobj`, int(p.ID), p.Kids, len(p.Kids)) + return err +} + +// Page represents a PDF page object with size DIN A4 +type Page struct { + Common + + Resources []Object // Image + Contents []Object // Common (streams) + + // Parent contains the human-readable name of the parent object, + // which will be translated into an object ID when encoding. + Parent string +} + +// Objects implements Object. +func (p *Page) Objects() []Object { + result := []Object{p} + for _, o := range p.Resources { + result = append(result, o.Objects()...) + } + for _, o := range p.Contents { + result = append(result, o.Objects()...) + } + return result +} + +// Encode implements Object. +func (p *Page) Encode(w io.Writer, ids map[string]ObjectID) error { + xObjects := make([]string, len(p.Resources)) + for idx, o := range p.Resources { + xObjects[idx] = fmt.Sprintf("/%s %v", o.Name(), ids[o.Name()]) + } + _, err := fmt.Fprintf(w, ` +%d 0 obj +<< + /Resources << + /XObject << +%s + >> + >> + /Contents %v + /Parent %v + /Type /Page + /MediaBox [ 0 0 152 152 ] +>> +endobj`, int(p.ID), strings.Join(xObjects, "\n"), p.Contents, ids[p.Parent]) + return err +} + +type countingWriter struct { + cnt int + w io.Writer +} + +func (cw *countingWriter) Write(p []byte) (n int, err error) { + n, err = cw.w.Write(p) + cw.cnt += n + return n, err +} + +// Encoder is a PDF writer. +type Encoder struct { + w *countingWriter +} + +// NewEncoder returns a ready-to-use Encoder writing to w. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + w: &countingWriter{w: w}, + } +} + +// writeXrefTable writes a cross-reference table to e.w. See also “PDF +// 32000-1:2008 PDF 1.7” section “7.5.4 Cross-Reference Table” +func (e *Encoder) writeXrefTable(Objects []Object, xrefOffsets []int) error { + if _, err := fmt.Fprintf(e.w, "\nxref\n0 %d\n", len(Objects)+1); err != nil { + return err + } + + // index 0 can never point to a valid Object, so print an invalid entry: + if _, err := fmt.Fprintf(e.w, "%010d %05d %s \n", 0, 65535, "f"); err != nil { + return err + } + + const generation = 0 + for _, offset := range xrefOffsets { + if _, err := fmt.Fprintf(e.w, "%010d %05d %s \n", offset, generation, "n"); err != nil { + return err + } + } + return nil +} + +// Encode writes the PDF file represented by the specified catalog. +func (e *Encoder) Encode(r *Catalog, info *DocumentInfo) error { + // As per “PDF Explained: How a PDF File is Written”: + // https://www.geekbooks.me/book/view/pdf-explained + + // (1.) Output the header. + + // Byte sequence 0xE2E3CFD3 as per the recommendation from + // “Developing with PDF”. See “Chapter 1. PDF Syntax”: + // https://www.safaribooksonline.com/library/view/developing-with-pdf/9781449327903/ch01.html#_header + if _, err := e.w.Write(append([]byte("%PDF-1.4\n%"), 0xe2, 0xe3, 0xcf, 0xd3)); err != nil { + return err + } + + // Flatten the Object graph into a slice + objects := append(r.Objects(), info.Objects()...) + + // (3.) Assign ids from 1 to n and store them in a lookup table + // (some Objects need to resolve name references when encoding). + ids := make(map[string]ObjectID, len(objects)) + for idx, obj := range objects { + id := ObjectID(idx + 1) + obj.SetID(id) + ids[obj.Name()] = id + } + + // (4.) Output the Objects one by one, starting with Object number + // one, recording the byte offset of each for the cross-reference + // table. + xrefOffsets := make([]int, len(objects)) + for idx, obj := range objects { + xrefOffsets[idx] = e.w.cnt + 1 + if err := obj.Encode(e.w, ids); err != nil { + return err + } + } + + // (5.) Write the cross-reference table. + xrefOffset := e.w.cnt + + if err := e.writeXrefTable(objects, xrefOffsets); err != nil { + return err + } + + // (6.) Write the trailer, trailer dictionary, and end-of-file marker. + if _, err := fmt.Fprintf(e.w, `trailer +<< + /Root %v + /Size %d + /Info %v +>> +startxref +%d +%%%%EOF +`, ids["catalog"], len(objects)+1, ids["info"], xrefOffset); err != nil { + return err + } + + return nil +} diff --git a/qrbill.go b/qrbill.go index c457d96..bd73fd5 100644 --- a/qrbill.go +++ b/qrbill.go @@ -302,6 +302,21 @@ func (b *Bill) EncodeToEPS() ([]byte, error) { return qrCodeEPS, nil } +func (b *Bill) EncodeToPDF() ([]byte, error) { + var err error + code, err := encoder.Encoder_encode(b.qrcontents, decoder.ErrorCorrectionLevel_M, qrEncodeHints()) + if err != nil { + return nil, err + } + + const quietzone = 4 + qrCodeEPS, err := renderResultPDF(code, qrCodeEdgeSidePx, qrCodeEdgeSidePx, quietzone) + if err != nil { + return nil, err + } + return qrCodeEPS, nil +} + func (b *Bill) EncodeToImage() (image.Image, error) { return generateSwissQrCode(b.qrcontents) } diff --git a/qrcodegeneratorpdf.go b/qrcodegeneratorpdf.go new file mode 100644 index 0000000..db2587e --- /dev/null +++ b/qrcodegeneratorpdf.go @@ -0,0 +1,194 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package qrbill + +import ( + "bytes" + "fmt" + "image" + "io" + "strings" + "time" + + "github.com/makiuchi-d/gozxing" + "github.com/makiuchi-d/gozxing/qrcode/encoder" + "github.com/stapelberg/qrbill/internal/pdf" +) + +// Image represents a PDF image object containing a DIN A4-sized page +// scanned with 600dpi (i.e. into 4960x7016 pixels). +type Image struct { + pdf.Common + + Bounds image.Rectangle +} + +// Objects implements Object. +func (i *Image) Objects() []pdf.Object { return []pdf.Object{i} } + +// Encode implements Object. +func (i *Image) Encode(w io.Writer, ids map[string]pdf.ObjectID) error { + _, err := fmt.Fprintf(w, ` +%d 0 obj +<< + /Subtype /Form + /FormType 1 + /Type /XObject + /ColorSpace /DeviceGray + /BBox [0 0 %d %d] + /Matrix [1 0 0 1 0 0] + /Resources << /ProcSet [/PDF] >> + /Length %d +>> +stream +%s +endstream +endobj`, + int(i.Common.ID), + i.Bounds.Max.X, + i.Bounds.Max.Y, + len(i.Common.Stream), + i.Common.Stream) + return err +} + +// renderResultPDF is a copy of renderResult from +// gozxing/qrcode/qrcode_writer.go, adapted to output to PDF. +func renderResultPDF(code *encoder.QRCode, width, height, quietZone int) ([]byte, error) { + input := code.GetMatrix() + if input == nil { + return nil, gozxing.NewWriterException("IllegalStateException") + } + inputWidth := input.GetWidth() + inputHeight := input.GetHeight() + qrWidth := inputWidth + (quietZone * 2) + qrHeight := inputHeight + (quietZone * 2) + outputWidth := qrWidth + if outputWidth < width { + outputWidth = width + } + outputHeight := qrHeight + if outputHeight < height { + outputHeight = height + } + + multiple := outputWidth / qrWidth + if h := outputHeight / qrHeight; multiple > h { + multiple = h + } + // Padding includes both the quiet zone and the extra white pixels to accommodate the requested + // dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone. + // If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will + // handle all the padding from 100x100 (the actual QR) up to 200x160. + leftPadding := (outputWidth - (inputWidth * multiple)) / 2 + topPadding := (outputHeight - (inputHeight * multiple)) / 2 + + _, _ = leftPadding, topPadding + + var codePath strings.Builder + codePath.WriteString("q\n") + + // Change the application coordinate system to work like the SVG one does, + // for consistency between the different code paths. See also General + // Coordinate System Transformation, Page 18, Encapsulated PostScript File + // Format Specification: + // https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf + codePath.WriteString("1 0 0 -1 0 1265 cm\n") + + for inputY, outputY := 0, topPadding; inputY < inputHeight; inputY, outputY = inputY+1, outputY+multiple { + // Write the contents of this row of the barcode + for inputX, outputX := 0, leftPadding; inputX < inputWidth; inputX, outputX = inputX+1, outputX+multiple { + if input.Get(inputX, inputY) == 1 { + codePath.WriteString(fmt.Sprintf("%d %d %d %d re\n", + outputX, outputY, multiple, multiple)) + //eps.WriteString(fmt.Sprintf("%d %d %d %d F\n", outputX, outputY, multiple, multiple)) + } + } + } + // Fill the whole path at once. + // This step is crucial: + // filling individual rectangles results in rendering artifacts + // in some PDF viewers at some zoom levels. + // Filling the whole path seems to prevent that entirely. + codePath.WriteString("0 g\n") + codePath.WriteString("f\n") + + // overlay a PDF version of the swiss cross + codePath.WriteString("1 0 0 1 549 549 cm\n") + + codePath.WriteString("1 g\n") // white + codePath.WriteString("0 0 166 166 re\n") + codePath.WriteString("f\n") + + codePath.WriteString("0 g\n") // black + codePath.WriteString("12 12 142 142 re\n") + codePath.WriteString("f\n") + + codePath.WriteString("1 g\n") // white + codePath.WriteString("36 66 94 28 re\n") + codePath.WriteString("f\n") + codePath.WriteString("68 34 30 92 re\n") + codePath.WriteString("f\n") + + codePath.WriteString("Q\n") + + kids := []pdf.Object{ + &pdf.Page{ + Common: pdf.Common{ObjectName: "page0"}, + Resources: []pdf.Object{ + &Image{ + Common: pdf.Common{ + ObjectName: "qr", + Stream: []byte(codePath.String()), + }, + Bounds: image.Rect(0, 0, 1265, 1265), + }, + }, + Parent: "pages", + Contents: []pdf.Object{ + &pdf.Common{ + ObjectName: "content0", + //[]byte("q 595.28 0 0 841.89 0.00 0.00 cm /code0 Do Q\n"), + Stream: []byte(`q +0.12 0 0 0.12 0 0 cm +/qr Do +Q +`), + }, + }, + }, + } + + doc := &pdf.Catalog{ + Common: pdf.Common{ObjectName: "catalog"}, + Pages: &pdf.Pages{ + Common: pdf.Common{ObjectName: "pages"}, + Kids: kids, + }, + } + info := &pdf.DocumentInfo{ + Common: pdf.Common{ObjectName: "info"}, + CreationDate: time.Now(), + Producer: "https://github.com/stapelberg/qrbill", + Title: "QR-Bill", + // TODO: summarize the qr code in the subject + } + var buf bytes.Buffer + pdfEnc := pdf.NewEncoder(&buf) + if err := pdfEnc.Encode(doc, info); err != nil { + return nil, err + } + return buf.Bytes(), nil +}