PDFKit PDF Generation¶
Relevant source files * package-lock.json * src/router.js
Purpose and Scope¶
This document covers the PDFKit-based programmatic PDF generation system that creates product reports with precise layout control. This approach directly generates PDF documents using drawing commands and coordinate positioning, contrasting with the HTML-to-PDF rendering approach. For the Puppeteer-based HTML rendering approach, see Puppeteer PDF Generation. For general PDF generation features, see PDF Generation.
The PDFKit implementation provides:
- Programmatic PDF creation without HTML templates
- Direct control over layout positioning and styling
- Streaming response for efficient memory usage
- Product data export functionality for authenticated users
Overview¶
The PDFKit PDF generation system creates product reports by programmatically drawing content onto a PDF document. Unlike the Puppeteer approach that renders HTML templates, PDFKit uses low-level drawing commands to place text at specific coordinates, providing precise control over layout but requiring manual positioning calculations.
Key Characteristics:
- Route:
GET /pdfkit/descargar - Authentication: Requires valid JWT via
verifyTokenmiddleware - Data Source:
productostable in MySQL database - Output Format: PDF file with filename
productos_desde_cero.pdf - Delivery Method: Direct streaming via
doc.pipe(res)
Sources: src/router.js L355-L396
Request Flow Architecture¶
flowchart TD
Client["Client Browser"]
Route["GET /pdfkit/descargar<br>router.js:355"]
VerifyToken["verifyToken<br>Middleware"]
DBQuery["db.query<br>SELECT * FROM productos"]
PDFDoc["new PDFDocument<br>margin: 40, size: A4"]
SetHeaders["res.setHeader<br>Content-Disposition<br>Content-Type"]
PipeStream["doc.pipe(res)"]
DrawTitle["doc.fontSize(18).text<br>Listado de Productos"]
DrawHeaders["Draw Table Headers<br>x,y coordinates"]
DrawRows["forEach productos<br>Draw data rows"]
EndDoc["doc.end()"]
Response["PDF Stream to Client"]
MySQL["MySQL Database<br>productos table"]
Client --> Route
Route --> VerifyToken
VerifyToken --> DBQuery
DBQuery --> PDFDoc
PDFDoc --> SetHeaders
SetHeaders --> PipeStream
PipeStream --> DrawTitle
DrawTitle --> DrawHeaders
DrawHeaders --> DrawRows
DrawRows --> EndDoc
EndDoc --> Response
Response --> Client
DBQuery --> MySQL
Sources: src/router.js L355-L396
Route Definition and Configuration¶
The PDFKit route is defined at src/router.js L355-L396
with the following structure:
| Aspect | Configuration |
|---|---|
| HTTP Method | GET |
| Path | /pdfkit/descargar |
| Authentication | verifyToken middleware |
| Authorization | User-level access (any authenticated user) |
| Database Query | SELECT * FROM productos |
| Response Type | application/pdf |
| Content-Disposition | attachment; filename="productos_desde_cero.pdf" |
Authentication Flow:
- Client must have valid JWT in
tokencookie verifyTokenmiddleware validates JWT signature and expiration- User information attached to
req.userfor potential logging - No role-based restriction (both users and admins can access)
Sources: src/router.js L355-L396
PDF Document Initialization¶
The PDF document is created with specific configuration parameters:
const doc = new PDFDocument({ margin: 40, size: 'A4' });
Configuration Parameters:
| Parameter | Value | Purpose |
|---|---|---|
margin |
40 |
Sets 40-point margin on all sides |
size |
'A4' |
Standard paper size (210mm × 297mm) |
Response Headers:
Content-Disposition: attachment; filename="productos_desde_cero.pdf"- Forces browser downloadContent-Type: application/pdf- Specifies MIME type
Streaming Architecture: The document is piped directly to the response object at src/router.js L367
:
doc.pipe(res);
This approach streams the PDF content as it's generated rather than buffering the entire document in memory, providing efficient memory usage for large reports.
Sources: src/router.js L361-L367
Content Rendering Process¶
flowchart TD
Start["Start Rendering"]
Title["Render Title<br>fontSize(18)<br>text('Listado de Productos')<br>align: center<br>moveDown()"]
HeaderFont["Set Header Font<br>font('Helvetica-Bold')<br>fontSize(12)"]
GetY["y = doc.y<br>Current vertical position"]
HeaderText["Draw Headers<br>Referencia (50, y)<br>Nombre (150, y)<br>Precio (300, y)<br>Stock (380, y)"]
IncrementY1["y += 20<br>Space after headers"]
DataFont["Set Data Font<br>font('Helvetica')<br>fontSize(11)"]
LoopStart["For each<br>producto"]
DrawRow["Draw Row Data<br>ref (50, y)<br>nombre (150, y)<br>precio (300, y)<br>stock (380, y)"]
IncrementY2["y += 20<br>Next row"]
LoopEnd["End Loop"]
Finalize["doc.end()"]
Start --> Title
Title --> HeaderFont
HeaderFont --> GetY
GetY --> HeaderText
HeaderText --> IncrementY1
IncrementY1 --> DataFont
DataFont --> LoopStart
LoopStart --> DrawRow
DrawRow --> IncrementY2
IncrementY2 --> LoopStart
LoopStart --> Finalize
Sources: src/router.js L369-L395
Layout Positioning System¶
The PDFKit implementation uses absolute positioning with fixed X-coordinates for table columns:
Column Layout¶
| Column | X Position | Content | Data Type |
|---|---|---|---|
| Referencia | 50 | p.ref.toString() |
Integer (AUTO_INCREMENT) |
| Nombre | 150 | p.nombre |
String (varchar(30)) |
| Precio | 300 | Number(p.precio).toFixed(2) |
Decimal (10,2) |
| Stock | 380 | p.stock.toString() |
Integer |
Vertical Positioning¶
The Y-coordinate is managed dynamically:
- Initial Position: Retrieved via
doc.yafter title at src/router.js L375 - Header Row: Headers placed at current Y position
- Row Spacing: Y incremented by 20 points between rows at src/router.js L382 and src/router.js L391
Coordinate System Diagram:
flowchart TD
P50["X=50<br>Referencia"]
P150["X=150<br>Nombre"]
P300["X=300<br>Precio"]
P380["X=380<br>Stock"]
Title["Title (centered)<br>Y = initial"]
Headers["Headers Row<br>Y = doc.y"]
Row1["Data Row 1<br>Y += 20"]
Row2["Data Row 2<br>Y += 20"]
RowN["Data Row N<br>Y += 20"]
Title --> Headers
Headers --> Row1
Row1 --> Row2
Row2 --> RowN
subgraph subGraph0 ["PDF Coordinate Space"]
P50
P150
P300
P380
P50 --> P150
P150 --> P300
P300 --> P380
end
Sources: src/router.js L373-L391
Font and Typography Configuration¶
Font Specifications¶
The implementation uses two font weights from the Helvetica family:
| Context | Font | Size | Code Location |
|---|---|---|---|
| Title | (default) | 18 | src/router.js L370 |
| Table Headers | Helvetica-Bold | 12 | src/router.js L374 |
| Table Data | Helvetica | 11 | src/router.js L384 |
Text Rendering Methods¶
Title Rendering:
doc.fontSize(18).text("Listado de Productos", { align: "center" }).moveDown();
- Method chaining for font size, content, and alignment
moveDown()adds vertical spacing after title
Positioned Text:
doc.text(content, x, y);
- Explicit X,Y coordinates for table cells
- No automatic line wrapping or overflow handling
Sources: src/router.js L370-L390
Data Processing and Type Conversion¶
Products retrieved from the database undergo type conversion before rendering:
Type Conversions¶
| Field | Database Type | Conversion | Reason |
|---|---|---|---|
ref |
INT(11) | p.ref.toString() |
PDFKit requires strings |
nombre |
VARCHAR(30) | p.nombre |
Already string |
precio |
DECIMAL(10,2) | Number(p.precio).toFixed(2) |
Format to 2 decimals |
stock |
INT(11) | p.stock.toString() |
PDFKit requires strings |
Iteration Pattern¶
The data rendering loop at src/router.js L386-L392
uses forEach to iterate through the results array:
results.forEach((p) => {
doc.text(p.ref.toString(), 50, y);
doc.text(p.nombre, 150, y);
doc.text(Number(p.precio).toFixed(2), 300, y);
doc.text(p.stock.toString(), 380, y);
y += 20;
});
Sources: src/router.js L386-L392
Database Integration¶
flowchart TD
Query["db.query<br>SELECT * FROM productos"]
Callback["Callback Function<br>(error, results)"]
ErrorCheck["error?"]
ErrorResponse["res.status(500)<br>Error al obtener productos"]
PDFGeneration["Generate PDF<br>with results"]
ProductosTable["productos table<br>ref (PK)<br>nombre<br>precio<br>stock"]
Query --> Callback
Callback --> ErrorCheck
ErrorCheck --> ErrorResponse
ErrorCheck --> PDFGeneration
Query --> ProductosTable
ProductosTable --> Callback
Query Details:
- SQL:
SELECT * FROM productos - Returns: Array of product objects
- Error Handling: Returns HTTP 500 on query failure
- Callback Pattern: Traditional callback at src/router.js L356-L396
Database Schema Reference:
For complete productos table schema, see productos Table.
Sources: src/router.js L356-L359
PDFKit vs Puppeteer Comparison¶
| Aspect | PDFKit | Puppeteer |
|---|---|---|
| Route | /pdfkit/descargar |
/pdf/descargar |
| Approach | Programmatic drawing | HTML template rendering |
| Template | None (code-based) | views/pdfTabla.ejs |
| Layout Method | Absolute coordinates | CSS styling |
| Memory Usage | Low (streaming) | Higher (browser instance) |
| Dependencies | PDFKit only | Puppeteer + EJS + Chrome |
| Flexibility | Manual positioning | HTML/CSS flexibility |
| Complexity | Low-level control | Higher abstraction |
| Performance | Fast (direct PDF) | Slower (browser startup) |
| Maintenance | Position calculations | Template maintenance |
When to Use Each Approach¶
PDFKit (Current Page):
- Simple tabular reports
- Known, fixed layouts
- Performance-critical scenarios
- Minimal external dependencies
Puppeteer (Page 10.1):
- Complex layouts with CSS
- Existing HTML templates
- Rich typography and graphics
- Need for print CSS features
Sources: src/router.js L355-L396
Complete Generation Flow Diagram¶
sequenceDiagram
participant Client
participant Router
participant verifyToken
participant MySQL Database
participant PDFDocument
participant Response Stream
Client->>Router: GET /pdfkit/descargar
Router->>verifyToken: Verify JWT token
verifyToken->>Router: User authenticated
Router->>MySQL Database: SELECT * FROM productos
MySQL Database->>Router: results array
Router->>PDFDocument: new PDFDocument({margin:40, size:'A4'})
Router->>Response Stream: Set headers (Content-Type, Content-Disposition)
Router->>PDFDocument: doc.pipe(res)
note over PDFDocument: Streaming begins
PDFDocument->>Response Stream: Write title
PDFDocument->>Response Stream: Write table headers
loop [For each producto]
PDFDocument->>Response Stream: Write row data
end
PDFDocument->>Response Stream: doc.end()
Response Stream->>Client: PDF file stream
Sources: src/router.js L355-L396
Error Handling¶
Database Query Errors¶
If the database query fails at src/router.js L357
:
if (error) {
return res.status(500).send("Error al obtener productos");
}
Error Response:
- HTTP Status: 500
- Content-Type: text/html
- Body: "Error al obtener productos"
Authentication Errors¶
Authentication failures are handled by the verifyToken middleware before reaching the route handler:
- Missing token → 401 response
- Invalid token → 401 response
- Expired token → 401 response
For authentication details, see verifyToken Middleware.
Sources: src/router.js L355-L359
Document Finalization¶
The PDF document is finalized at src/router.js L394
:
doc.end();
Finalization Process:
doc.end()signals end of content writing- Remaining buffered content flushed to stream
- PDF structure finalized (xref table, trailer)
- Stream closed automatically
- Client receives complete PDF file
No Explicit Response Closure:
The response is closed automatically when the piped stream ends. The route handler does not explicitly call res.end() because the stream handles this.
Sources: src/router.js L394
Usage Example¶
Client Request:
GET /pdfkit/descargar HTTP/1.1
Host: example.com
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Server Response:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="productos_desde_cero.pdf"
Content-Length: [file size]
%PDF-1.3
[binary PDF content]
Browser Behavior:
The attachment disposition triggers a file download dialog with the suggested filename productos_desde_cero.pdf.
Sources: src/router.js L364-L365
Implementation Dependencies¶
NPM Packages¶
| Package | Version | Purpose |
|---|---|---|
pdfkit |
0.17.1 | PDF document generation |
express |
5.1.0 | HTTP routing framework |
mysql2 |
3.14.1 | Database connectivity |
jsonwebtoken |
9.0.2 | JWT authentication |
Internal Dependencies¶
src/middlewares/verifyToken.js- JWT validationdatabase/db.js- MySQL connection poolprocess.env.JWT_SECRET- Token signing key
Sources: package-lock.json L27-L28
Performance Considerations¶
Memory Efficiency¶
Streaming Approach:
- PDF content is piped directly to response stream
- No intermediate buffer for entire document
- Constant memory usage regardless of document size
- Suitable for large product catalogs
Comparison Metrics¶
| Metric | PDFKit | Puppeteer |
|---|---|---|
| Startup Time | ~0ms (library load) | ~500-1000ms (browser launch) |
| Memory Overhead | ~10-20MB | ~100-200MB |
| CPU Usage | Low | High (rendering engine) |
| Concurrency | High (lightweight) | Limited (browser instances) |
Sources: src/router.js L355-L396
Limitations and Constraints¶
Layout Limitations¶
- Fixed Positioning: All coordinates are hardcoded
- No Overflow Handling: Text exceeding column width is not wrapped
- No Pagination: Large datasets may exceed single page
- Single Page Assumption: No automatic page breaks
Data Constraints¶
Column Width Assumptions:
- Referencia: Assumes short integer values
- Nombre: Maximum 30 characters (database constraint)
- Precio: Assumes standard currency format
- Stock: Assumes reasonable integer values
Missing Features¶
- No page headers/footers
- No page numbering
- No automatic table continuation across pages
- No grid lines or borders
- No sorting or filtering options
Sources: src/router.js L373-L391
Future Enhancement Possibilities¶
Pagination Support¶
To handle large datasets, the implementation could:
- Track vertical position (
ycoordinate) - Detect when approaching page bottom
- Call
doc.addPage()to create new page - Repeat headers on each page
Dynamic Column Widths¶
Calculate column widths based on:
- Actual content length
- Available page width
- Font metrics
Styling Enhancements¶
- Add table borders and grid lines
- Alternate row background colors
- Header background styling
- Footer with timestamp or page numbers
Sources: src/router.js L355-L396