Spis treści

Automatyczne "podpisywanie"dokumentów

Jedną z zalet posiadania trójki dzieci jest ciągła konieczność mikrooptymalizacji różnych około-domowych procesów. Wyzwaniem jest ogarnianie zakupów, posiłków i prania, a czasmi dochodzą jeszcze różne “jednorazowe” (ale przecież cykliczne) zadania, na które jeszcze nie mam stworzonego procesu, a które irytują mnie samym faktem swojego istnienia.

Jednym z takich zadań jest cotygodniowe wysyłanie do zastępowej ZHR dokumentu, w którym swierdzam, że w tym tygodniu moja córka dobrze się czuje i może brać udział w zbiórce harcerskiej.

Co tydzień muszę usiąść do komputera, rozłożyć tablet graficzny, otworzyć w prorgamie Gimp dokument pdf, podpisać go ręcznie, wpisując również datę, a następnie wyeksportować stronę z podpisem jako obraz i skleić ją wraz z innymi stronami-obrazami przy użyciu narzędzia convert, aby uzyskać dokument .pdf, który wysyłam do zastępowej mailem.

Zastanawiałam się wielokrotnie, czy tego procesu nie uprościć. Miałam jednak wątpliwości: czy, jeśli wygeneruję takiego .pdf-a automatycznie, nastapił akt “podpisania”? Po namyśle doszłam do wniosku, że tak naprawdę to sam fakt wysłania dokumentu do osoby prowadzącej zbiórkę jest wyrazem czy też potwierdzeniem mojej woli. Podpis nałożony automatem niewiele się różni od rzeczywiście złożonego, a z prawnego punktu widzenia pewnie tak samo nic nie znaczy.

Dobrze by było stworzyć małe narządko, któremu podam datę, a ono wypełni mi odpowiednie pola i wygeneruje gotowy “podpisany” pdf.

Co chcę zrobić?

Chcę na podstawie dokumentu pdf zawierającego formularz automatycznie wygenerować inny dokument pdf, w którym na określonej pozycji pojawi się imię i nazwisko mojej córki, a na innej ręcznie wpisana data i mój własnoręczny podpis.

Jak to zrobić?

Utworzenie obrazków

Potrzebuję kilku obrazków:

  • imienia i nazwiska dziecka
  • mojego imienia i nazwiska
  • obrazków z cyframi
  • obrazka z kropką (data będzie w formacie dd.MM.rrr)

Uruchamiam Gimp-a, otwieram pdf i sprawdzam, ile mam miejsca na formularzu.

Okno programu Gimp z narzędziem zaznaczenia prostokątnego na dokumencie pdf

Tworzę dwa obrazki o odpowiednich wymiarach (mój podpis oraz imię i nazwisko córki) oraz serię obrazków przedstawiających cyfry (które zapisuję w plikach o nazwach reprezentujących te ctfry, za wyjątkiem kropki, którą zapisuję w pliku d.png):

Okno programu Gimp z warstwami przedstawiającymi kolejne cyfry

Utworzenie projektu

Uruchamiam IntellijIdea, tworzę projekt mavenowy i dodaję zależności. Do generowania pdf-a w świecie Javy służy biblioteka itext, ale dopiero po chwili orientuję się, że sprawy są skomplikowane:

  • wersja 5 to jeden jar, który początkowo włączyłam do projetu
  • wersja 7 to wersja zmodularyzowana (jest dostępniona na licencji AGPL) Na stronie z porównaniem obydwu wersji można poczytać o tym, co każda z nich oferuje.

Postanowiłam trzymać się wersji 7, tym bardziej, że to dla tej wersji znalazłam:

Dodanie zależności:

Oto zależności, których użyłam:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 <dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>kernel</artifactId>
    <version>7.2.1</version>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>layout</artifactId>
    <version>7.2.1</version>
</dependency>

W zasadzie to mój programik powstał po przeczytaniu jednego przykładu dodawanie znaków wodnych do obrazków i przejrzeniu kilku innych. Nigdy wcześniej nie korzystałam z biblioteki itext.

Położenie obrazków

Czas nałożyć obrazek na dokument pdf. API, z którego skorzystałam, wymaga podania współrzędnych bezwzględnych. Układ współrzędnych ma swój początek (0, 0) w lewym, dolnym rogu, ale współrzędne punktu, w którym miał być nałożony obrazek, a jakie ustaliłam w Gimpie zostały chyba inaczej zinterpretowane i nie wyrenderowały się tam, gdzie się ich spodziewałam. Dlatego właśnie całe to pozycjonowanie odbywało się metodą prób i błędów.

Do wyrenderowania daty użyłam obrazków-cyfr, układając je w poziomie obok siebie. Oddaliłam je od siebie o pewną dobraną na oko wielkość. Używam datę podaną w wierszu poleceń bądź - jeśi jej nie podano - datę dzisiejszą.

Program

Założenia

Program zakłada, że w katalogu resources znajdują się:

  • plik input.pdf zawierający pusty, dwustronicowy formularz

  • pliki 0.png, 1.png, …, 9.png oraz d.png (z kropką)

    • które będą nakładane co 15 pikseli, począwszy od współrzędnych (dateLeft, dateBottom), czyli (200, 580)
  • plik signature.png z moim podpisem, który zostanie nałożony

    • na współrzędnych (350, 580)
    • na stronie 2
    Odręczny podpis
  • plik kid_name.png z imieniem i nazwiskiem córki (w dopełniaczu), który zostanie nałożony

    • na współrzędnych (50, 245)
    • na stronie 1 Odręczny podpis o - imię i nazwisko dziecka napisane ręcznie"/>

Do bieżącego katalogu wygenerowany zostanie plik output.pdf.

Kod

Kod jest krótki i robi, co trzeba.

 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
package com.kamilachyla.pdfstamp;

import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;

import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public class Main {
    public static final String OUTPUT = "output.pdf";
    public static final String INPUT = "input.pdf";

    record Position(String img, int page, int left, int bottom){}

    static void generate(String optDateStr) throws Exception {
        final var inputStream = Objects.requireNonNull(Main.class.getResourceAsStream("/" + INPUT));
        PdfDocument pdfDoc = new PdfDocument(new PdfReader(inputStream), new PdfWriter(OUTPUT));
        Document doc = new Document(pdfDoc);
        var dateLeft = 200;
        var dateBottom = 580;
        var letterWidth = 15;
        var dateStr = Optional.ofNullable(optDateStr).orElse(
                LocalDate.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));
        var positions = new ArrayList<Position>();
    
        positions.addAll(List.of(
                new Position("/signature.png", 2, 350, 580),
                new Position("/kid_name.png", 1, 50, 245)));
        for (int i = 0; i < dateStr.length(); i++) {
            final var charAt = dateStr.charAt(i);
            var f = "/%c.png".formatted(charAt == '.' ? 'd' : charAt);
            positions.add(new Position(f, 2, dateLeft + i * letterWidth, dateBottom));
        }

        for (Position p : positions) {
            Image image = new Image(ImageDataFactory.create(Objects.requireNonNull(Main.class.getResource(p.img))));
            image.setFixedPosition(p.page, p.left, p.bottom);
            doc.add(image);
        }

        doc.close();
    }

    public static void main(String[] args) {
        try {
            Main.generate(args[0]);
            System.out.printf(new File(OUTPUT).getAbsolutePath());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Uruchomienie

Przy uruchomieniu można podać datę w formacie dd.MM.yyy. Uruchomienie bez daty używa daty bieżącej.

1
java -jar target/pdfstamp-1.0-SNAPSHOT-jar-with-dependencies.jar 22.03.2022
Fragment wygenerowanego dokumentu

Ładniejszy pdf

Wygenerowany pdf byłby ładniejszy, gdybym na przykład zainwestowała więcej czasu i stworzyła własną czcionkę, imitującą mój charakter pisma. Artykuł Tworzenie czcionek w Inkscape pokazuje, jak to zrobić krok po kroku.

Początkowo miałam ochotę od tego zacząć, ale w końcu zdecydowałam, że zrobię prototyp wklejając zwykłe rastry.

Nad piękną czcionką może jeszcze kiedyś się zastanowię…

Github

Kod wrzuciłam do repozytorium na githubie.