Jump to content
Jens Martsch - dotnetic

How to render admin page into a variable to create a PDF?

Recommended Posts

I want to create PDFs of certain pages in the admin. I do not want to recreate the admin template on the frontend and write code to show every field that I have in my template. Instead I would like to get just the HTML of the admin page in a variable so I can use it for generating my PDFs.

How would I do that? Can it be done via API? Or use ob_start to capture the output?

Share this post


Link to post
Share on other sites

If you hook after Page::render you can assign $event->return to a variable and use that.

  • Like 2

Share this post


Link to post
Share on other sites

What exactly are you trying to do? I guess you want to create some sort of reports? https://parall.ax/products/jspdf could also be an option. But generating a PDF from the markup of the adminpage will definitely need some additional work. I'm curious what you come up with. Could also need a good solution 😉

  • Like 1

Share this post


Link to post
Share on other sites

@bernhard I try to generate PDFs of one one or more admin pages (forms). I built an application with PW where people can enter details (for example a job application or a request form for office material) and there are many fields (150+) in the admin. Also in the admin I ordered and positioned the fields like they should appear on the resulting PDF. So I do not want to make a new template on the frontend level, just to recreate what I already have in the admin. If fields are added I have to change nothing.

So I hope to come up with a good solution. I already seen your RockPDF module, and also Pages2PDF and PDFcomposer. Thanks for the additional hint.

Share this post


Link to post
Share on other sites

I guess it would be the easiest to setup a function that loops over all fields of the edit form, creates a simple HTML table (or floating divs could also be an option) and populates all the values with outputformatting turned on.

Rendering the original admin HTML into pdf will bring you a lot of bloat and I guess it will be hard to make that look pretty (actually I think it will be hard to even make it work at all, at least with RockPDF (mpdf), because it does not support all kinds of css and you also have some JS magic in the pw admin...).

  • Like 1

Share this post


Link to post
Share on other sites
31 minutes ago, bernhard said:

Rendering the original admin HTML into pdf will bring you a lot of bloat

@jmartsch Based on my limited amount of experience in converting HTML into PDF I can also confirm that keeping it vector based and simply converting the HTML/CSS/JS code of the admin into PDF will possibly not work out well. However, you might want to try turning it into a JPEG (or JPEGs), and afterwards embedding them into a multipage PDF. I have no idea if it is doable or not, but there are libraries out there, such as the one pupped up in my google seach: https://github.com/tsayen/dom-to-image

Share this post


Link to post
Share on other sites

There are different libs out there which convert existing HTML and CSS to a PDF. If I create a Bitmap graphic out of the website, the PDF would not be searchable. So converting to a bitmap is not a solution for me.

Looping over the fields and generating own markup would be the exact same process as if I did it on the frontend. I have to take every single fieldtype into account and render it accordingly. This is one thing ProcessWire's admin already does. Although this solution would be a lot cleaner with simpler HTML. Then I would create an own stylesheet and generate the PDF out of it. As I said before, I would not like to do this. But it might be an option.

  • Like 1

Share this post


Link to post
Share on other sites
37 minutes ago, jmartsch said:

But my main question remains: How do I get the HTML of the page. Most important is, how do I get it when I am not actually on the page. Because I want to get the HTML of multiple pages.

13 hours ago, adrian said:

If you hook after Page::render you can assign $event->return to a variable and use that.

As adrian already told you. Or like this:

w87Q490.png

  • Like 1

Share this post


Link to post
Share on other sites

I have seen Adrian's answer but to hook into the rendering, I need to render the page, as you did in your example. 

The problem with your example is, that it does not get the HTML of the admin page, but instead of a template file that resides in site/templates. But I need the HTML from the admin page.

Share this post


Link to post
Share on other sites

@jmartsch: are these admin pages regular pages open in ProcessPageEdit, custom Process modules or pages added through AdminCustomFiles? In each of these cases, the answers might be a bit different.

  • Like 1

Share this post


Link to post
Share on other sites
2 minutes ago, BitPoet said:

@jmartsch: are these admin pages regular pages open in ProcessPageEdit, custom Process modules or pages added through AdminCustomFiles? In each of these cases, the answers might be a bit different.

They are regular admin pages. Then in a module I would like to add a button that gathers the HTML of the admin pages and generates one or multiple PDFs out of it.

Share this post


Link to post
Share on other sites
15 hours ago, jmartsch said:

I have to take every single fieldtype into account and render it accordingly.

I'd say this is the way to go for sure. There aren't that many fieldtypes that require special treatment, and this way you get exactly the markup you want. You can get the inputfield widths used in Page Edit too so the layout is similar. A starting point...

$out = '';
$p = $pages(1234); // get the page however you like
$fieldgroup = $p->fields;
foreach($fieldgroup as $field) {
    /* @var Field $field */
    $field = $fieldgroup->getFieldContext($field);
    $inputfield = $field->getInputfield($p);
    $width = $inputfield->columnWidth ?: 100;
    // getFieldValue() is a function/method you create that returns a string (could be markup) value for $field on $p, depending on the type of $field
    $value = getFieldValue($p, $field);
    $out .= "<div class='field-value' style='width: {$width}%'>$value</div>";
}

I think it's unlikely that the admin markup is going to render nicely when you convert it to PDF. In my experience the HTML-to-PDF conversion tools don't handle complex layouts well. Plus there is going to be all kinds of markup in Page Edit that won't be relevant to what you want to show in your PDF (tabs, buttons, "Add New" links for Repeaters, etc).

 

If you do want to try HTML-to-PDF conversion for PW's default markup value for fields, or using the Page Edit markup, then here are some avenues to explore. Your mileage may vary as I only tested these quickly. You'll need to include the admin CSS.

// Use PW's default markup value for different field types
$out = '';
$p = $pages(1234); // get the page however you like
foreach($p->fields as $field) {
    $out .= $p->getMarkup($field->name);
}
// Get markup for the whole Page Edit form
$input->post->id = 1234; // the ID of the page you want
$ppe = $modules->ProcessPageEdit;
$out = $ppe->execute();
// Get markup for just the Content tab (you can't easily show more than one tab in the PDF anyway)
$input->post->id = 1234; // the ID of the page you want
$ppe = $modules->ProcessPageEdit;
$form = $ppe->buildFormContent();
$out = $form->render();

 

  • Like 5
  • Thanks 1

Share this post


Link to post
Share on other sites

Thank you all for your insights and information. Most people think, that it would not be a smart thing to render the default admin markup. However I am still evaluating options and trying out different things. Thanks @Robin S for his code.

At a later time I would like to get an export of many PDFs (100 or 1000, I don't know yet, how many my customer would handle). I tried to find the perfect tool for generating the PDF. I tried WeasyPrint (written in Python) which has very good rendering capabilities. The problem is, that I have to call it from PHP via exec(), and it gives no feedback or errors. Additionally WeasyPrint requires an URL or a file to render to PDF, but does not know anything about my login status in ProcessWire. As my PDFs and pages are protected and should only be visible to logged in users, this is not a real solution 😞

Another problem is performance. The generation of 100 PDFs from pages would take a long time. First the page with over 150 fields has to be rendered and then the PDF has to be created. Multiply the time with 100 and you are waiting a looooong time. 

So right now, I think I use the Pages2PDF module which generates a PDF for a page on save and saves it to disk.

As a template for the PDF I will try Robin's code to iterate through the Inputfields and get a clean output.

When a user then wants to get an export of all PDFs, I just have to gather the files and zip them or combine them into one PDF. This would be really fast.

What do you guys think about this approach?

Share this post


Link to post
Share on other sites

Do you know about RockPdf?

Yeah, performance is a problem with all those PHP PDF libraries. Generating PDFs on page save would definitely help, if possible. mPDF also has a feature to replace text in pdf files, so you could maybe copy a base PDF and replace blocks individually to make it more performant (never tried, though). The problem with this feature (for me) was that it does only support strings and not any other markup (like images).

Share this post


Link to post
Share on other sites

Hello,

there is also one "alternate" solution, using Node.js and Puppeteer with headless browser.
In this case, export to PDF is only one segment what can be done with that tools (remote login, automated processing, deep testing, etc...).

If you have Node.js on your machine, here is example (Windows) where project directory "printer" is in C partition (C:\printer).

C:\>

mkdir printer
cd printer
npm i puppeteer easy-pdf-merge

After this, inside project directory are all required node modules (Puppeteer, Chromium browser, Easy PDF).

Last step is to create index.js file and place it inside project directory ( C:\printer )

// index.js

const puppeteer = require('puppeteer');
const merge = require('easy-pdf-merge');

// configuration
// *** EDIT THIS:
var admin_url = "http://my_site.com/admin_url";
var user = '*****';
var pasw = '*****';

// desired pages
// *** EDIT THIS:
var pdfUrls = [
               "/page/edit/?id=1054&modal=1",
               "/page/edit/?id=1016&modal=1",
			   "/page/edit/?id=1019&modal=1",
               "/setup/field/edit?id=1#inputfieldConfig",
               "/setup/field/edit?id=1&modal=1#inputfieldConfig"
			   ];

var pdfFiles = [];

// START
async function main(){

  const browser = await puppeteer.launch({headless: true, args:['--start-maximized']});
  const page = await browser.newPage();
  await page.setViewport({width: 1366, height: 768});

  // login
  await page.goto(admin_url, { waitUntil: 'networkidle0' });
  await page.type('#login_name', user);
  await page.type('#login_pass', pasw);
  // login submit
  await Promise.all([
            page.click('#Inputfield_login_submit'),
            page.waitForNavigation({ waitUntil: 'networkidle0' })
  ]);

  for(var i = 0; i < pdfUrls.length; i++){
    await page.goto(admin_url + pdfUrls[i], {waitUntil: 'networkidle0'});
    var pdfFileName =  'page' + (i + 1) + '.pdf';
    pdfFiles.push(pdfFileName);
    await page.pdf({ path: pdfFileName, format: 'A4', printBackground: true,margin: {top: 0, right: 0, bottom: 0, left: 0}});
  }

  await browser.close();
  await mergeMultiplePDF(pdfFiles);
};

const mergeMultiplePDF = (pdfFiles) => {
    return new Promise((resolve, reject) => {
        merge(pdfFiles,'processwire.pdf',function(err){
            if(err){
                console.log(err);
                reject(err)
            }
            console.log('Success');
            resolve()
        });
    });
};

// run all this and exit
main().then(process.exit);


*** Note: edit this script and write your login parameters and desired urls.

After all, run script  (inside C:\printer>)

node index.js

After a while (for this example, ~10 sec.) you will find PDF files in project folder (partials and 1 merged with all).
As example here is attachment.

Regards.

processwire.pdf

  • Like 4
  • Thanks 1

Share this post


Link to post
Share on other sites

Wow @OLSA thx for that great example! Creating PDFs via PHP (mpdf) definitely has some drawbacks (formatting is sometimes tedious since not all css commands are supported, for example you can't use block elements inside table cells). Your method would also work with charts or other complex elements. Not sure how multipage would work, but I guess support for that via the @page directives should be possible.

@jmartsch For creating 1000s of reports you could use RockGrid's batcher. You could create a grid containing all elements to create a PDF from and batcher would create the reports one-by-one with a progressbar and user feedback, similar to this:

reports.gif

  • Like 2

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...