DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • A Practical Guide to Creating a Spring Modulith Project
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Spring Boot Secured By Let's Encrypt

Trending

  • Ensuring Configuration Consistency Across Global Data Centers
  • Next-Gen IoT Performance Depends on Advanced Power Management ICs
  • AI Speaks for the World... But Whose Humanity Does It Learn From?
  • Event-Driven Microservices: How Kafka and RabbitMQ Power Scalable Systems
  1. DZone
  2. Coding
  3. Frameworks
  4. Adding Rich Output Options to a Full-Stack Application Using DocRaptor

Adding Rich Output Options to a Full-Stack Application Using DocRaptor

Learn how to add DocRaptor to convert HTML to a PDF with advanced features, such as custom headers, footers, watermarks, and endless styling options.

By 
John Vester user avatar
John Vester
DZone Core CORE ·
Nov. 13, 20 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
73.8K Views

Join the DZone community and get the full member experience.

Join For Free

Building modern applications has become easier for feature teams, because of wonderful frameworks like Spring Boot, Angular, ReactJS, and Vue.  Services from Amazon, Heroku, Microsoft, and even Google have provided exciting options to further compliment the modern application experience.  However, the need for a rich output format (PDF or Microsoft Excel [XLS] ) seems to remain a few steps behind everything else.  Complex designs in CSS are often the go-to architecture, however, these can become challenging to understand and support.

One would think this is a clear indicator that rich output is becoming less of a need as the application experience evolves.  However, these needs continue to remain on the feature list from applications of all sizes.  In fact, the Fitness Application I am currently building needs to produce invoices in a PDF format. Tax-preparation software requires print quality output for either manual filing or for future reference.  XLS extractions are a must for applications that provide a great deal of data.

After realizing that rich output is still a valid need, I investigated a product called DocRaptor.

About DocRaptor

I first looked into several open source solutions for creating my PDFs, but quickly realized they couldn't handle the complexity, nor the (hopeful!) scale that I would need.  Instead I found what seemed like a perfect solution: DocRaptor.  It is based on Prince, an industry-proven, feature-rich engine that performs the conversion; however, Prince does not have a service offering on top to provide easy access to this engine.

DocRaptor is a SaaS solution on top of the engine with APIs that support various languages, including:

  • C#
  • Java
  • Node
  • PHP
  • Python
  • Ruby

There is also a RESTful URI, which can be called using a simple cURL command:

Java
 




x


 
1
curl http://YOUR_API_KEY_HERE@docraptor.com/docs \
2
 --fail --silent --show-error \
3
 --header "Content-Type:application/json" \
4
 --data '{"test": true,
5
          "document_url": "http://www.docraptor.com/examples/invoice.html",
6
          "type": "pdf" }' > docraptor.pdf


In the example above, the template data in the invoice.html file is used to produce a simple PDF called docraptor.pdf.  You can even try the cURL command above, which will produce a PDF similar to below:

The Need

To try out DocRaptor, I wanted to create a full-stack application experience using Spring Boot and Angular.  The Spring Boot RESTful service would introduce a service to provide a list classic rock and roll performers.  The Angular application would display these performing artists in a table format.

On the same display, I added a button to create a PDF version of the list.  The use of this button will make a RESTful call to the Spring Boot API.  In turn, the DocRaptor SaaS solution will generate a PDF version of the table output.  The example PDF will test the capabilities of the convertor engine by duplicating the table across two pages with the following features:

Page #1

The first page will contain the following elements:

  • utilize portrait orientation
  • a watermark (but only available when test-mode is set to false)
  • lower-case roman numeric page numbering
  • page numbering at the bottom of the page (footer)

Page #2

In order to demonstrate the power and flexibility with DocRaptor, the second page will contain::

  • landscape orientation
  • no watermark will appear on the second page
  • two-digit standard page numbering
  • page numbering at the top of the page (header)

The Approach

DocRaptor expects HTML format to produce PDF or XLS documents.  In this example, the Spring Boot service will act as the integration point for DocRaptor.  As such, Spring Boot must gather the necessary data and format the source as HTML so that DocRaptor can return it as a byte[].

Here are the contents from the https://docraptor.com/examples/invoice.html DocRaptor file:

HTML
 




xxxxxxxxxx
1
114


 
1
<!DOCTYPE html>
2
<html>
3
  <head>
4
    <title>Your New Project for Our Best Client</title>
5
    <meta data-fr-http-equiv="content-type" content="text/html; charset=utf-8" />
6
    <style type="text/css">
7
      /*resets from YUI*/
8
      table {border-collapse:collapse; border-spacing:0;}
9

          
10
      /* setup the page */
11
      @page { margin: 30px; background: #ffffff; }
12
      /* setup the footer */
13
      @page { @bottom { content: flow(foot); } }
14
      #footer { flow: static(foot); }
15

          
16
      /* useful utility */
17
      .clear { clear:both; }
18

          
19
      /* layout */
20
      #container { font-family: Omnes Light, Trebuchet MS, Calibri, Futura, Geneva, Tahoma; font-size: 14pt; color: #a7a7a7; position: relative; }
21

          
22
      /* footer shenanigans! */
23
      #footer { text-align: center; display: block; }
24

          
25
      /* colors */
26
      .black { color: black }
27

          
28
      /* stylin */
29

          
30
      #quote_name { margin-top: 3.5em; text-align: right; font-weight: bold; font-size: 1.5em }
31

          
32
      #client { font-size: 0.75em; margin-top: 3em; margin-left: 0.5em;}
33

          
34
      #client_header { font-size: 0.5em; }
35

          
36
      #phase_details {
37
        margin-top: 2em;
38
        font-size: 0.6em;
39
        border-width: 1px;
40
        border-spacing: 0px;
41
        border-style: solid;
42
        border-color: gray;
43
        width: 100%;
44
      }
45

          
46
      #phase_details th { font-size: 0.8em; padding: 10px !important; border-style: solid !important; }
47

          
48
      #phase_details th, td {
49
        border-width: 1px;
50
        padding: 3px 5px;
51
        border-top-style: none;
52
        border-bottom-style: none;
53
        border-left-style: solid;
54
        border-right-style: solid;
55
        border-color: gray;
56
        background-color: white;
57
      }
58

          
59
      #phase_details tr.first td { padding-top: 10px; padding-bottom: 10px; }
60

          
61
      #phase_details td.price { text-align: left; }
62
      #phase_details .price_container { float: left; min-width: 30%; }
63

          
64
      #phase_details thead .title { width: 20%; }
65
      #phase_details thead .description { width: 60%; }
66
      #phase_details thead .price { width: 20%; }
67
      #phase_details tr.last { border-bottom: 1px solid gray; }
68
      #footer #contain { text-align: right; font-size: 0.8em; }
69
      #total_price { text-align: right; margin-right: 6.75em; margin-top: 0.5em; }
70
      #total_price h2 { color: black; font-size: 0.6em; font-weight: bold; }
71
      #total_price .price { margin-left: 0.75em; }
72
    </style>
73
  </head>
74
  <body>
75
    <div id="container">
76
      <div id="logo">Your Logo</div>
77
      <div id="main">
78
        <div id="header">
79
          <div id="header_info black">1234 Made Up LN <span class="black">|</span> (555)-555-5555 <span class="black">|</span> example.com</div>
80
        </div>
81
        <h1 class="black" id="quote_name">Your New Project</h1>
82
        <div id="client">
83
          <div id="client_header">client:</div>
84
          <p class="address black">
85
            Our Best Cient
86
          </p>
87
        </div>
88
        <table id="phase_details">
89
          <thead>
90
            <tr>
91
              <th class="title">phase title</th>
92
              <th class="description">phase description &amp; features</th>
93
              <th class="price">price</th>
94
            </tr>
95
          </thead>
96
          <tr class="first black">
97
            <td>When We Do Stuff</td>
98
            <td>From 10/10/2010 to 11/11/2011</td>
99
            <td class="price"><div class="price_container">$300</div></td>
100
            </tr>
101
          <tr>
102
            <td></td>
103
            <td>Doing Stuff</td>
104
            <td class="price"><div class="price_container">$200</div></td>
105
          </tr>
106
          ...
107
        </table>
108
      </div>
109
      <div id="total_price">
110
        <h2>TOTAL: <span class="price black">$1100</span></h2>
111
      </div>
112
    </div>
113
  </body>
114
</html>


Everything that DocRaptor needs to convert HTML to a PDF is within this file.  Of course, it is possible to link to external styling files to avoid duplicating efforts.

Setting Up the Spring Boot Service With DocRaptor

Adding DocRaptor to a Spring Boot instance is as simple as adding the following dependency:

XML
 




xxxxxxxxxx
1


 
1
        <dependency>
2
            <groupId>com.docraptor</groupId>
3
            <artifactId>docraptor</artifactId>
4
            <version>2.0.0</version>
5
        </dependency>



The Spring Boot service for this example includes an in-memory H2 database and is populated with a small list of classic rock and roll artists.  For this data, the following Entity object exists:

Java
 




xxxxxxxxxx
1
10


 
1
@Data
2
@Entity
3
@Table(name = "artists")
4
public class Artist {
5
    @Id
6
    private String name;
7
    private String yearFormed;
8
    private boolean active;
9
    private String imageUrl;
10
}


When combined with a RESTful URI in the ArtistController, a simple GET command to the \artists path provides data similar to the list below:

Java
 




xxxxxxxxxx
1


 
1
[
2
 ...
3
 {
4
   "name": "Yes",
5
   "yearFormed": "1968",
6
   "active": true,
7
   "imageUrl": "https://somehost/Yes_concert.jpg"
8
 }
9
]


Configuring DocRaptor Inside Spring Boot

The following attributes are required to interact with the DocRaptor service:

  • ${DOCRAPTOR_API_KEY} - API key provided for a new account [doc-raptor.api-key]
  • ${DOCRAPTOR_TEST_MODE_ENABLED} - determines if test mode is enabled (see below) [doc-raptor.test-mode]
  • ${DOCRAPTOR_USE_JAVASCRIPT} - determines if Javascript is enabled [doc-raptor.use-javascript]
  • ${PORT} - port for Spring Boot service [server.port] (optional, default is 8080)

About doc-raptor.test-mode

By enabling test mode via the ${DOCRAPTOR_TEST_MODE_ENABLED} variable, which maps to [doc-raptor.test-mode], no charges will be incurred against the DocRaptor account.  However, enabling test mode replaces any custom watermarks with a generic DocRaptor watermark.

Keep in mind, it is possible to use a [doc-raptor.api-key] equal to YOUR_API_KEY_HERE. However, in doing so [doc-raptor.test-mode] will always be set to true within the DocRaptor service.

Starting Spring Boot With DocRaptor

With the DocRaptor configuration in place, starting the Spring Boot server with the sample project should yield a start-up screen similar to below:

After  loading, log entries similar to those below should appear:

Java
 




xxxxxxxxxx
1


 
1
2020-10-30 10:42:05.100  INFO 95816 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2
2020-10-30 10:42:05.108  INFO 95816 --- [           main] c.g.j.d.DocraptorServiceApplication      : Started DocraptorServiceApplication in 2.615 seconds (JVM running for 3.042)
3
2020-10-30 10:42:15.634  INFO 95816 --- [nio-8017-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
4
2020-10-30 10:42:15.634  INFO 95816 --- [nio-8017-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
5
2020-10-30 10:42:15.639  INFO 95816 --- [nio-8017-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms


Creating a Dynamic Template for DocRaptor

In order to create the template for DocRaptor to use, I decided to use j2html.   j2html is an HTML builder that allows  Java programs to produce HTML text.  I added the library to Spring Boot via the following dependency:

XML
 




xxxxxxxxxx
1


 
1
        <dependency>
2
            <groupId>com.j2html</groupId>
3
            <artifactId>j2html</artifactId>
4
            <version>1.4.0</version>
5
        </dependency>


Next, I created a very simple static utility class for this example:

Java
 




xxxxxxxxxx
1
96


 
1
public final class HtmlBuilder {
2
    private HtmlBuilder() { }
3

          
4
    public static String createHtml(List<Artist> artists, String title) {
5
        ContainerTag containerTag = html(
6
                        head(
7
                                title(title),
8
                                style(createStyleObjects()).attr("type", "text/css")
9
                        ),
10
                        body(
11
                                div(
12
                                    div("DRAFT").attr("id", "watermark"),
13
                                    h1(title),
14
                                    createBasicTable(artists.stream())
15
                                ).withClass("named_page_one"),
16
                                div(
17
                                    h1(title),
18
                                    createBasicTable(artists.stream())
19
                                ).withClass("named_page_two page_break")
20
                        )
21
        );
22

          
23
        return containerTag.render();
24
    }
25

          
26
    private static ContainerTag createBasicTable(Stream<Artist> artists) {
27
        return table(
28
                thead(
29
                        tr(
30
                            th(text("Photo")),
31
                            th(text("Name")),
32
                            th(text("Founded")),
33
                            th(text("Active"))
34
                        )
35
                ),
36
                tbody(
37
                        artists.map(artist ->
38
                                tr(
39
                                    td("").attr("width", "5%").attr("background", artist.getImageUrl()).withStyle("background-size: contain; background-repeat: no-repeat;"),
40
                                    td(text(artist.getName())),
41
                                    td(text(artist.getYearFormed())),
42
                                    td(text(artist.isActive() ? "Yes" : "No"))
43
                                )).toArray(ContainerTag[]::new)
44
                )
45
        );
46
    }
47

          
48
    private static String createStyleObjects() {
49
        return "table, th, td {" +
50
                "border: 1px solid black; " +
51
                "padding: 7px; " +
52
                "} " +
53
                "table {" +
54
                "  border-collapse: collapse;" +
55
                "  width: 100%;" +
56
                "} " +
57

          
58
                "#watermark {" +
59
                "flow: static(watermarkflow);" +
60
                "font-size: 120px;" +
61
                "opacity: 0.5;" +
62
                "transform: rotate(-30deg);" +
63
                "text-align: center;" +
64
                "} " +
65

          
66
                "@page namedPage1 {" +
67
                "size: letter portrait;" +
68
                "@bottom {" +
69
                "content: counter(page, lower-roman);" +
70
                "} " +
71
                "@prince-overlay {" +
72
                "content: flow(watermarkflow)" +
73
                "} " +
74
                "} " +
75

          
76
                "@page namedPage2 {" +
77
                "size: letter landscape;" +
78
                "margin-top: 70px; " +
79
                "@top {" +
80
                "content: counter(page, decimal-leading-zero);" +
81
                "} " +
82
                "} " +
83

          
84
                ".named_page_one {" +
85
                "page: namedPage1;" +
86
                "} " +
87

          
88
                ".named_page_two {" +
89
                "page: namedPage2;" +
90
                "} " +
91

          
92
                ".page_break {" +
93
                "page-break-before: always;" +
94
                "} ";
95
    }
96
}


While very light on cascading style sheets (CSS) features, this HtmlBuilder class will create the template required by DocRaptor and even allow Artist data to be part of the artifact that will be created.

Creating a DocRaptor Service

You can process the DocRaptor request via a simple public method within the DocRaptorService:

Java
 




xxxxxxxxxx
1
10


 
1
public byte[] process(DocRaptorRequest docRaptorRequest) throws Exception {
2
        log.info("process(docRaptorRequest={}, testMode={})", docRaptorRequest, docRaptorProperties.isTestMode());
3

          
4
        DocApi docApi = new DocApi();
5
        ApiClient apiClient = docApi.getApiClient();
6
        apiClient.setUsername(docRaptorProperties.getApiKey());
7

          
8
        validateRequest(docRaptorRequest);
9
        return docApi.createDoc(buildDocFromRequest(docRaptorRequest));
10
    }



Once the validateRequest verifies the docRaptorRequest it calls the buildDocFromRequest() method:

Java
 




xxxxxxxxxx
1
23


 
1
private Doc buildDocFromRequest(DocRaptorRequest docRaptorRequest) {
2
        Doc doc = new Doc();
3
        doc.setTest(docRaptorProperties.isTestMode());
4
        doc.setJavascript(docRaptorProperties.isUseJavascript());
5

          
6
        if (docRaptorRequest.getContentType().equals(DocRaptorContentType.STRING)) {
7
            doc.setDocumentContent(docRaptorRequest.getDocumentContent());
8
        } else {
9
            doc.setDocumentUrl(docRaptorRequest.getDocumentUrl());
10
        }
11

          
12
        doc.setDocumentType(docRaptorRequest.getDocumentType());
13
        doc.setName(docRaptorRequest.getName());
14

          
15
        if (docRaptorRequest.usePrinceXml()) {
16
            PrinceOptions princeOptions = new PrinceOptions();
17
            princeOptions.setBaseurl(docRaptorRequest.getBaseUrl());
18
            doc.setPrinceOptions(princeOptions);
19
        }
20

          
21
        log.debug("doc={}", doc.toString());
22
        return doc;
23
    }


Within the ArtistService, the following method interfaces with the DocRaptor service:

Java
 




xxxxxxxxxx
1


 
1
public byte[] createPdf(String name) throws Exception {
2
        DocRaptorRequest docRaptorRequest = new DocRaptorRequest();
3
        docRaptorRequest.setDocumentType(Doc.DocumentTypeEnum.PDF);
4
        docRaptorRequest.setContentType(DocRaptorContentType.STRING);
5
        docRaptorRequest.setDocumentContent(HtmlBuilder.createHtml(getArtists(), "Classic Rock Artists"));
6
        docRaptorRequest.setName(name);
7
        return docRaptorService.process(docRaptorRequest);
8
    }


Then, the ArtistController and the /artists/pdf URI call the service:

Java
 




xxxxxxxxxx
1
18


 
1
    @GetMapping(value = "/artists/pdf", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
2
    public ResponseEntity<byte[]> getArtistsPdf(@RequestParam(required = false) String filename) {
3
        try {
4

          
5
            if (StringUtils.isEmpty(filename)) {
6
                filename = "Artists.pdf";
7
            }
8

          
9
            HttpHeaders headers = new HttpHeaders();
10
            headers.setCacheControl(CacheControl.noCache().getHeaderValue());
11
            headers.add("content-disposition", "inline;filename=" + filename);
12

          
13
            return new ResponseEntity<>(artistService.createPdf(filename), headers, HttpStatus.OK);
14
        } catch (Exception e) {
15
            log.error(e.getMessage(), e);
16
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
17
        }
18
    }


Adding a Simple Angular Client

At this point, you can use a simple cURL or Postman request to retrieve the PDF file from DocRaptor via the following command:

Shell
 




xxxxxxxxxx
1


 
1
curl --location --request GET 'http://localhost:8080/artists/pdf'


However, I wanted to create a very basic Angular application to provide some presentation around the experience.  After building a simple list-artists component, the following screen appears when performing an ng serve from the Angular repository:

This view is a simple list of data from the in-memory H2 database of the Spring Boot service.  The Create PDF located in the top right-hand corner of the application calls DocRaptor and the artists/pdf URI. 

Requesting a PDF

In order for DocRaptor to make the PDF file, the Create PDF button is wired to the following method:

Java
 




xxxxxxxxxx
1


 
1
  createPdf() {
2
    this.artistsService.createPdf().subscribe((response)=> {
3
      let file = new Blob([response], {type: 'application/pdf'});
4
      let fileURL = URL.createObjectURL(file);
5
      window.open(fileURL);
6
    });
7
  }


This method calls the artistsService in Angular:

Java
 




xxxxxxxxxx
1


 
1
  createPdf() {
2
    const httpOptions = {
3
      'responseType'  : 'arraybuffer' as 'json'
4
    };
5

          
6
    return this.http.get<any>(this.baseUrl + '/pdf', httpOptions);
7
  }


This, in turn, calls Spring Boot, which contacts DocRaptor and returns a PDF in a new window:


Success! In the example above, we see a PDF file. 

As you will see, the first page contains the following elements:

  • portrait orientation
  • a watermark (but only available when test-mode is set to false)
  • lower-case roman numeric page numbering
  • page numbering at the bottom of the page (footer)

The second page contains the following elements: 

  • landscape orientation
  • no watermark appears on the second page
  • two-digit standard page numbering
  • page numbering at the top of the page (header)

Conclusion

In this article, we explored how to create a simple PDF of data contained within a full-stack application using DocRaptor.   If you are interested in the data for this example, check out the following repository:

https://gitlab.com/johnjvester/doc-raptor

Initially, I was concerned that I wouldn't be able to provide a path to an Angular template for  DocRaptor to build the PDF using HTML data.  However,  I quickly realized that is not a DocRaptor use case.  While I can provide invoice information to my client as part of the application, I don't need a great deal of the printed invoice content inside the view within an application.  Content such as customer and provider information are required when providing a print-quality version of the data.

This rich template data needs to be stored somewhere. Furthermore, the ability to dynamically create these templates fits nicely with the niche that DocRaptor meets.  Of course, the same use case also exists for XLS files.

From a pricing perspective, getting started is free by using the API_KEY=YOUR_API_KEY_HERE in the header of each request.  This will generate a PDF or XML file with all the necessary features but will add a "TEST DOCUMENT" watermark in the output.

Getting started with a free seven-day trial allows the testMode=false functionality to remove such watermarks and allow a custom watermark similar to the one provided in this example.  After the free trial, the Basic plan costs $15 (USD) a month for 125 documents per month.  The price-per-document reduces significantly at the Silver level, which allows for 40,000 documents at a rate of $1,000 (USD) per month.  There are some additional pricing options, including an unlimited version.

In today's modern application development world, SaaS options provide a mechanism to do something really well at an attractive price.  This allows feature teams to focus on refining business rules and enhancing intellectual property logic within their application.

Have a really great day!

application Spring Framework Spring Boot

Opinions expressed by DZone contributors are their own.

Related

  • A Practical Guide to Creating a Spring Modulith Project
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Spring Boot Secured By Let's Encrypt

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!