Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
PDFs erstellen unter ASP.NET Core
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5961
Herkunft: Leipzig

Themenstarter:

PDFs erstellen unter ASP.NET Core

beantworten | zitieren | melden

Hallo allerseits,

ich bin auf der Suche nach einer Möglichkeit, PDF-Dateien aus Templates + Daten zu erstellen. Diese Dateien sollen mehrseitig, möglichst barrierefrei zugänglich sein, und mit Seiten-Headern und -Footern ausgestattet sein. Das ganze soll in einer ASP.NET Core Web-Anwendung unter Linux laufen.

Bisher haben wir dafür das Reporting-Tool von DevExpress verwendet. Dafür hat man das Layout in einem proprietären Editor zusammengefrickelt, und dann die Daten per "DataBinding" in das Layout gefrickelt. Im Zuge der Umstellung auf .NET 5 wollen wir davon wegmigrieren.

Ich war ein bißchen ernüchtert, nachdem ich diesen Artikel gelesen hatte: Creating A PDF In .NET Core. Jemand hat dort die aktuellen (2019) Möglichkeiten evaluiert. Die Kommentare darunter sind auch sehr aufschlußreich. Offenbar gibt es kein Tool von kostenlos bis extrem teuer, das unsere Anforderungen abdeckt. Entweder gibt es keine Lösung für .NET Core, oder es läuft nicht unter Linux, oder es werden bei Seitenumbrüchen die Hälfte der letzten Zeile auf Seite 1 und die andere Hälfte der Zeile auf Seite 2 gedruckt, was für professionelle Dokumente natürlich nicht akzeptabel ist.

Und alle Bibiotheken, die kein Templating unterstützen, wo man also jedes Element selbst auf der Seite positionieren muß, kommt für unsere Zwecke nicht in Frage.

Meine erste Idee war daher, ein HTML- bzw. Razor-Template zu verwenden, und das dann in einem Headless-Chromium-Browser auf dem Server in ein PDF zu rendern. Aber leider eignet sich HTML nicht als Druckformat, denn sich auf allen Seiten wiederholende Header und Footer werden z.B. derzeit nur mit sehr häßlichen Workarounds unterstützt.

Eine andere Idee ist, mit Hilfe des Open XML SDK ein Word-Dokument zu erstellen, und das dann mittels LibreOffice per Kommandozeile in ein PDF zu konvertieren. Das scheint die einzige Möglichkeit zu sein, die alle unsere Anforderungen abdeckt. Aber vom Aufwand her übertrifft das noch das Erstellen von DevExpress-Reports.

Hat jemand etwas ähnliches schon im Einsatz? Oder gibt es andere Ideen?
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
gfoidl
myCSharp.de - Team

Avatar #avatar-2894.jpg


Dabei seit:
Beiträge: 7528
Herkunft: Waidring

beantworten | zitieren | melden

Hallo MrSparkle,

ist XSL-FO eine Option?
Würde zu Templates + Daten ganz gut passen und wenn die "Einstiegshürde" geschafft ist, so ist es relativ trivial zu handhaben. Z.B. mit https://www.nuget.org/packages/Fonet.Standard/

PS: mich wundert es auch (immer wieder) dass es für den de-facto Dokument-Standard PDF keine vernüftigen Lösungen gibt.

mfG Gü
Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

Zwei Dinge fallen mir ein:

- SyncFusion hat ne .NET Standard Lib
- Gibt zig Docker Images für PDF-Generierung

Letzteres hab ich schon so umgesetzt, dass ASP.NET Core ein Event Message geschossen hat, auf die eine Azure Function gehört hat.
Die Function hat dann die Daten gegen den PDF Container geworfen und die PDF generieren lassen.

In Process sollte man halt die PDF ohne Queue nicht generieren lassen, weil alle Libs schon relativ speicherhungrig sind.
private Nachricht | Beiträge des Benutzers
JimStark
myCSharp.de - Member

Avatar #dOpLzh7hN1az1g0eGRc0.jpg


Dabei seit:
Beiträge: 225

beantworten | zitieren | melden

Zitat von MrSparkle
Eine andere Idee ist, mit Hilfe des Open XML SDK ein Word-Dokument zu erstellen, und das dann mittels LibreOffice per Kommandozeile in ein PDF zu konvertieren. Das scheint die einzige Möglichkeit zu sein, die alle unsere Anforderungen abdeckt. Aber vom Aufwand her übertrifft das noch das Erstellen von DevExpress-Reports.

Genau so mache ich das bei einem Projekt. Hatte da aber das Problem dass es LibreOffice doch nochmal anders darstellt als Word. War ziemliches Gefrickel.
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5961
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Danke für die schnellen Antworten!
Zitat von gfoidl
ist XSL-FO eine Option?

Ich glaube, das ist das, wonach ich gesucht hatte. Eine Auszeichnungssprache für Druckdokumente. Ein erster Test hat gezeigt, daß es macht, was es soll. Jetzt werde ich mal ein komplexeres Layout umsetzen.

Danke!
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5961
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Ich hab mich entschieden, die PDF-Erstellung mit XSL-FO in Kombination mit Razor umzusetzen.

XSL-FO ist als Format genau dafür geschaffen worden, Druckdokumente zu erstellen, und ist leicht zu verstehen, wenn man mit HTML und CSS Erfahrung hat. Hier gibt es die Doku und ein paar Tutorials. Und mit Razor als Template-Engine kann man aus Daten einfach Reports erstellen, das kenne ich noch von ASP.NET MVC.

Ich verwende dazu das Package RazorEngineCore.

So kann man damit ein Template kompilieren und auswerten:


// Compiling is only done once
IRazorEngine razorEngine = new RazorEngine();
var compiledTemplate = await razorEngine.CompileAsync<RazorEngineTemplateBase<TModel>>(templateContent)
    .ConfigureAwait(false);

// Resolving a FO template for the specified model
string foTemplateResult = await compiledTemplate.RunAsync(instance =>
    {
        instance.Model = model;
    }).ConfigureAwait(false);

Für das Rendern des Templates in eine PDF verwende ich das Package Fonet.Standard (Source Code, Wiki). Das funktioniert soweit sehr gut. Ein paar Einschränkungen muß man dafür allerdings hinnehmen:
  • Bilder mit transparentem Hintergrund werden nicht unterstützt
  • SVGs werden nicht unterstützt (aber andere Vektor-Formate)
  • Tabellen mit automatischen Spaltenbreiten sind nicht möglich
  • Es können nur Schriftarten verwendet werden, die im Betriebssystem installiert sind

So funktioniert das Rendern der PDF:


var foNetDriver = FonetDriver.Make();
foNetDriver.Options = /* Set PDF options */;
foNetDriver.CloseOnExit = false; // Don't close stream after rendering
foNetDriver.ImageHandler = /* Set callback to load images from files or resources */;
foNetDriver.Render(foInputStream, pdfOutputStream);

Insgesamt sind das ca. 100 Zeilen Code in einer .NET-Standard-Bibliothek, die wir zukünftig für Web- und Desktop-Anwendungen verwenden können.

Das Template sieht z.B. so aus:

<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">  
  <fo:layout-master-set>
    <fo:simple-page-master master-name="simple" page-height="29.7cm" page-width="21cm" margin-top="2cm" margin-bottom="2cm" margin-left="2cm" margin-right="2cm">
      <fo:region-body region-name="page-body" margin-bottom="2cm" margin-top="2cm" />
      <fo:region-before region-name="page-header" extent="2cm" />
      <fo:region-after region-name="page-footer" extent="2cm" />
    </fo:simple-page-master>
  </fo:layout-master-set>
  
  <fo:page-sequence master-reference="simple" font-family="Montserrat">
    <fo:static-content flow-name="page-header">
      <fo:block font-family="Montserrat SemiBold" font-size="22pt">
        <fo:external-graphic src="logo.png" height="22pt" margin-right="1cm" />
        @Model.CompanyName
      </fo:block>
    </fo:static-content>
    
    <fo:static-content flow-name="page-footer">
      <fo:block margin-top="2cm" text-align="right" font-style="italic" color="#777777">
        @Model.Copyright
      </fo:block>
    </fo:static-content>
    
    <fo:flow flow-name="page-body">

      <fo:table table-layout="fixed" 
                inline-progression-dimension="auto"
                width="100%" 
                padding-after="2cm"
                font-size="8pt">
        <fo:table-column column-number="1" column-width="5cm" />
        <fo:table-column column-number="2" />
        <fo:table-column column-number="3" />
        <fo:table-column column-number="4" />
        <fo:table-column column-number="5" />

        <fo:table-header border-after-style="solid" 
                         border-after-color="black" 
                         border-after-width="thin">
          <fo:table-row font-weight="bold">
            <fo:table-cell>
              <fo:block>
                Mitarbeiter
              </fo:block>
            </fo:table-cell>
            <fo:table-cell>
              <fo:block>
                Datum
              </fo:block>
            </fo:table-cell>
            <fo:table-cell>
              <fo:block>
                Position
              </fo:block>
            </fo:table-cell>
            <fo:table-cell>
              <fo:block>
                Team
              </fo:block>
            </fo:table-cell>
            <fo:table-cell>
              <fo:block>
                Email
              </fo:block>
            </fo:table-cell>
          </fo:table-row>
        </fo:table-header>
        
        <fo:table-body>
          @{ int i = 0; }
          @foreach (var item in Model.Employees)
          {
            <fo:table-row background-color="@(++i % 2 == 0 ? "white" : "#f7f7f7")">
              <fo:table-cell>
                <fo:block>
                  @item.Salutation @item.FirstName @item.LastName 
                </fo:block>
              </fo:table-cell>
              <fo:table-cell>
                <fo:block>
                  @item.DateOfEntry.ToString("dd.MM.yy")
                </fo:block>
              </fo:table-cell>
              <fo:table-cell>
                <fo:block>
                  @item.WorkPosition
                </fo:block>
              </fo:table-cell>
              <fo:table-cell>
                <fo:block>
                  @item.Team
                </fo:block>
              </fo:table-cell>
              <fo:table-cell>
                <fo:block>
                  <fo:basic-link external-destination="mailto:@item.EmailAdress">@item.EmailAdress</fo:basic-link>
                </fo:block>
              </fo:table-cell>
            </fo:table-row>
          }
        </fo:table-body>
      </fo:table>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

Wenn die Schema-Datei im gleichen Verzeichnis liegt wie die Template-Dateien, bekommt man in Visual Studio sogar Intellisense für XSL-FO.

Das mag vielleicht ein bißchen over-engineered aussehen, ist aber wesentlich besser als DevExpress- und Crystal-Reports oder die Konvertierung von HTML- oder Word-Dateien.
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers