habulaj commited on
Commit
e5c3fa4
·
verified ·
1 Parent(s): d619856

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +615 -0
app.py CHANGED
@@ -5,6 +5,8 @@ from pydantic import BaseModel
5
  from weasyprint import HTML, CSS
6
  from io import BytesIO
7
  import base64
 
 
8
 
9
  app = FastAPI()
10
 
@@ -22,6 +24,475 @@ class PDFRequest(BaseModel):
22
  css: str = ""
23
  orientation: str = "portrait" # portrait or landscape
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  @app.get("/")
26
  def greet_json():
27
  return {"Hello": "World!"}
@@ -111,5 +582,149 @@ async def generate_pdf(request: PDFRequest):
111
  }
112
  )
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  except Exception as e:
115
  raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}")
 
5
  from weasyprint import HTML, CSS
6
  from io import BytesIO
7
  import base64
8
+ from typing import Optional, List, Dict, Any
9
+ from html import escape
10
 
11
  app = FastAPI()
12
 
 
24
  css: str = ""
25
  orientation: str = "portrait" # portrait or landscape
26
 
27
+ # Modelos para a nova API baseada em JSON
28
+ class Element(BaseModel):
29
+ id: str
30
+ type: str # a4, column, row, container, text, image, line, spacer, table
31
+ children: Optional[List[str]] = None
32
+ parentId: Optional[str] = None
33
+
34
+ # Propriedades comuns
35
+ padding: Optional[str] = None
36
+ width: Optional[str] = None
37
+ height: Optional[str] = None
38
+ backgroundColor: Optional[str] = None
39
+ backgroundImage: Optional[str] = None
40
+ backgroundImageBoxFit: Optional[str] = None
41
+ borderRadius: Optional[str] = None
42
+ alignment: Optional[str] = None
43
+ horizontalAlignment: Optional[str] = None
44
+ gap: Optional[str] = None
45
+
46
+ # Propriedades específicas de cada tipo
47
+ content: Optional[str] = None # text
48
+ color: Optional[str] = None # text
49
+ fontFamily: Optional[str] = None # text
50
+ fontSize: Optional[str] = None # text
51
+ fontWeight: Optional[str] = None # text
52
+ textAlign: Optional[str] = None # text
53
+ lineHeight: Optional[str] = None # text
54
+ wordSpacing: Optional[str] = None # text
55
+ letterSpacing: Optional[str] = None # text
56
+ fontStyle: Optional[str] = None # text
57
+ textDecoration: Optional[str] = None # text
58
+ textPadding: Optional[str] = None # text
59
+ maxLines: Optional[int] = None # text
60
+
61
+ src: Optional[str] = None # image
62
+ imageBoxFit: Optional[str] = None # image
63
+ opacity: Optional[float] = None # image
64
+
65
+ direction: Optional[str] = None # line (horizontal/vertical)
66
+ thickness: Optional[str] = None # line
67
+ lineColor: Optional[str] = None # line (renomeado para evitar conflito com color do text)
68
+
69
+ borderWidth: Optional[str] = None # container
70
+ borderStyle: Optional[str] = None # container
71
+ borderColor: Optional[str] = None # container
72
+ containerMargin: Optional[str] = None # container
73
+ expansion: Optional[str] = None # container
74
+
75
+ mainAxisSize: Optional[str] = None # column
76
+
77
+ columns: Optional[List[Dict[str, Any]]] = None # table
78
+ rows: Optional[List[Dict[str, Any]]] = None # table
79
+ headerBackgroundColor: Optional[str] = None # table
80
+ cellBackgroundColor: Optional[str] = None # table
81
+
82
+ class Project(BaseModel):
83
+ name: Optional[str] = "Documento A4"
84
+ orientation: Optional[str] = "portrait"
85
+
86
+ class PDFFromJSONRequest(BaseModel):
87
+ elements: List[Element]
88
+ project: Optional[Project] = None
89
+
90
+ # Funções auxiliares para gerar HTML a partir de JSON
91
+ def get_background_size(box_fit: Optional[str]) -> str:
92
+ """Converte boxFit para CSS background-size"""
93
+ if not box_fit:
94
+ return "cover"
95
+ size_map = {
96
+ "fill": "100% 100%",
97
+ "contain": "contain",
98
+ "cover": "cover",
99
+ "fitWidth": "100% auto",
100
+ "fitHeight": "auto 100%",
101
+ "none": "auto",
102
+ "scaleDown": "contain"
103
+ }
104
+ return size_map.get(box_fit, "cover")
105
+
106
+ def format_style_value(value: Any) -> str:
107
+ """Formata valores de estilo para CSS"""
108
+ if value is None:
109
+ return ""
110
+ if isinstance(value, (int, float)):
111
+ return f"{value}px"
112
+ if isinstance(value, str):
113
+ if any(unit in value for unit in ["px", "rem", "em", "%", "mm"]):
114
+ return value
115
+ try:
116
+ num = float(value)
117
+ return f"{num}px"
118
+ except ValueError:
119
+ return value
120
+ return str(value)
121
+
122
+ def generate_element_html(element: Element, elements_dict: Dict[str, Element], level: int = 0) -> str:
123
+ """Gera HTML para um elemento e seus filhos recursivamente"""
124
+ indent = " " * level
125
+
126
+ # Busca filhos
127
+ children = []
128
+ if element.children:
129
+ for child_id in element.children:
130
+ if child_id in elements_dict:
131
+ children.append(elements_dict[child_id])
132
+
133
+ element_type = element.type
134
+
135
+ if element_type == "a4":
136
+ styles = []
137
+ if element.backgroundColor:
138
+ styles.append(f"background-color: {element.backgroundColor}")
139
+ if element.backgroundImage:
140
+ styles.append(f"background-image: url({element.backgroundImage})")
141
+ styles.append(f"background-size: {get_background_size(element.backgroundImageBoxFit)}")
142
+ styles.append("background-position: center")
143
+ styles.append("background-repeat: no-repeat")
144
+
145
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
146
+ children_html = "\n".join([generate_element_html(child, elements_dict, level + 1) for child in children])
147
+ return f'{indent}<div class="a4-page"{style_attr}>\n{children_html}\n{indent}</div>'
148
+
149
+ elif element_type == "column":
150
+ styles = []
151
+ if element.padding and element.padding != "0":
152
+ styles.append(f"padding: {format_style_value(element.padding)}")
153
+ if element.gap and element.gap != "0":
154
+ styles.append(f"gap: {format_style_value(element.gap)}")
155
+ if element.width and element.width != "100%":
156
+ styles.append(f"width: {element.width}")
157
+
158
+ # Alignment
159
+ if element.alignment:
160
+ justify_map = {
161
+ "top-aligned": "flex-start",
162
+ "centered": "center",
163
+ "bottom-aligned": "flex-end",
164
+ "space-around": "space-around",
165
+ "space-between": "space-between"
166
+ }
167
+ justify = justify_map.get(element.alignment, "flex-start")
168
+ styles.append("align-items: stretch")
169
+ styles.append(f"justify-content: {justify}")
170
+
171
+ # Flex e height
172
+ is_root = element.id == "root-column"
173
+ if element.mainAxisSize == "max" or is_root:
174
+ styles.append("flex: 1 1 auto")
175
+ styles.append("height: 100%")
176
+ else:
177
+ styles.append("flex: 0 1 auto")
178
+ styles.append("height: auto")
179
+
180
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
181
+ children_html = "\n".join([generate_element_html(child, elements_dict, level + 1) for child in children])
182
+ return f'{indent}<div class="column"{style_attr}>\n{children_html}\n{indent}</div>'
183
+
184
+ elif element_type == "row":
185
+ styles = []
186
+ if element.padding and element.padding != "0":
187
+ styles.append(f"padding: {format_style_value(element.padding)}")
188
+ if element.width and element.width != "100%":
189
+ styles.append(f"width: {element.width}")
190
+ if element.gap and element.gap != "0":
191
+ styles.append(f"gap: {format_style_value(element.gap)}")
192
+ if element.alignment:
193
+ styles.append(f"align-items: {element.alignment}")
194
+
195
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
196
+ children_html = "\n".join([generate_element_html(child, elements_dict, level + 1) for child in children])
197
+ return f'{indent}<div class="row"{style_attr}>\n{children_html}\n{indent}</div>'
198
+
199
+ elif element_type == "container":
200
+ styles = []
201
+ if element.backgroundColor and element.backgroundColor != "transparent":
202
+ styles.append(f"background-color: {element.backgroundColor}")
203
+ if element.backgroundImage:
204
+ styles.append(f"background-image: url({element.backgroundImage})")
205
+ styles.append(f"background-size: {get_background_size(element.backgroundImageBoxFit)}")
206
+ styles.append("background-position: center")
207
+ styles.append("background-repeat: no-repeat")
208
+ if element.width:
209
+ styles.append(f"width: {format_style_value(element.width)}")
210
+ if element.height:
211
+ styles.append(f"height: {format_style_value(element.height)}")
212
+ if element.padding and element.padding != "0":
213
+ styles.append(f"padding: {format_style_value(element.padding)}")
214
+ if element.borderRadius and element.borderRadius != "0":
215
+ styles.append(f"border-radius: {format_style_value(element.borderRadius)}")
216
+
217
+ # Horizontal alignment
218
+ if element.horizontalAlignment:
219
+ align_map = {
220
+ "left-aligned": "0px auto 0px 0px",
221
+ "centered": "0px auto",
222
+ "right-aligned": "0px 0px 0px auto"
223
+ }
224
+ margin = align_map.get(element.horizontalAlignment, "0px auto")
225
+ styles.append(f"margin: {margin}")
226
+ elif element.containerMargin and element.containerMargin != "0":
227
+ styles.append(f"margin: {format_style_value(element.containerMargin)}")
228
+
229
+ # Border
230
+ if element.borderWidth and element.borderWidth != "0":
231
+ styles.append(f"border-width: {format_style_value(element.borderWidth)}")
232
+ styles.append(f"border-style: {element.borderStyle or 'solid'}")
233
+ if element.borderColor:
234
+ styles.append(f"border-color: {element.borderColor}")
235
+ else:
236
+ styles.append("border-width: 0px")
237
+ styles.append("border-style: none")
238
+
239
+ styles.append("box-sizing: border-box")
240
+ if element.expansion == "expanded":
241
+ styles.append("flex: 1 1 0%")
242
+ else:
243
+ styles.append("flex: 0 0 auto")
244
+
245
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
246
+ children_html = "\n".join([generate_element_html(child, elements_dict, level + 1) for child in children])
247
+ return f'{indent}<div class="container"{style_attr}>\n{children_html}\n{indent}</div>'
248
+
249
+ elif element_type == "text":
250
+ styles = []
251
+ if element.color:
252
+ styles.append(f"color: {element.color}")
253
+ if element.fontFamily:
254
+ styles.append(f"font-family: {element.fontFamily}")
255
+ if element.fontSize:
256
+ styles.append(f"font-size: {format_style_value(element.fontSize)}")
257
+ if element.fontWeight:
258
+ styles.append(f"font-weight: {element.fontWeight}")
259
+ if element.textAlign:
260
+ styles.append(f"text-align: {element.textAlign}")
261
+ if element.lineHeight:
262
+ styles.append(f"line-height: {element.lineHeight}")
263
+ if element.wordSpacing:
264
+ styles.append(f"word-spacing: {format_style_value(element.wordSpacing)}")
265
+ if element.letterSpacing:
266
+ styles.append(f"letter-spacing: {format_style_value(element.letterSpacing)}")
267
+ if element.fontStyle:
268
+ styles.append(f"font-style: {element.fontStyle}")
269
+ if element.textDecoration:
270
+ styles.append(f"text-decoration: {element.textDecoration}")
271
+ if element.textPadding and element.textPadding != "0":
272
+ styles.append(f"padding: {format_style_value(element.textPadding)}")
273
+ if element.maxLines:
274
+ styles.append(f"-webkit-line-clamp: {element.maxLines}")
275
+
276
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
277
+ content = escape(element.content or "")
278
+ return f'{indent}<p class="text"{style_attr}>{content}</p>'
279
+
280
+ elif element_type == "image":
281
+ styles = []
282
+ if element.width:
283
+ styles.append(f"width: {format_style_value(element.width)}")
284
+ if element.height:
285
+ styles.append(f"height: {format_style_value(element.height)}")
286
+ if element.padding and element.padding != "0":
287
+ styles.append(f"padding: {format_style_value(element.padding)}")
288
+ if element.borderRadius and element.borderRadius != "0":
289
+ styles.append(f"border-radius: {format_style_value(element.borderRadius)}")
290
+ if element.containerMargin and element.containerMargin != "0":
291
+ styles.append(f"margin: {format_style_value(element.containerMargin)}")
292
+ if element.imageBoxFit:
293
+ styles.append(f"object-fit: {element.imageBoxFit}")
294
+ if element.opacity is not None and element.opacity != 1:
295
+ styles.append(f"opacity: {element.opacity}")
296
+ if element.horizontalAlignment:
297
+ align_map = {
298
+ "left": "0",
299
+ "right": "auto",
300
+ "centered": "auto"
301
+ }
302
+ margin_left = align_map.get(element.horizontalAlignment, "auto")
303
+ margin_right = align_map.get(element.horizontalAlignment, "auto")
304
+ styles.append(f"display: block; margin-left: {margin_left}; margin-right: {margin_right}")
305
+
306
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
307
+ src = element.src or ""
308
+ return f'{indent}<img class="image" src="{src}" alt=""{style_attr} />'
309
+
310
+ elif element_type == "line":
311
+ styles = []
312
+ if element.lineColor:
313
+ styles.append(f"background-color: {element.lineColor}")
314
+ if element.thickness:
315
+ if element.direction == "vertical":
316
+ styles.append(f"width: {format_style_value(element.thickness)}")
317
+ else:
318
+ styles.append(f"height: {format_style_value(element.thickness)}")
319
+ if element.width and element.direction == "horizontal":
320
+ styles.append(f"width: {format_style_value(element.width)}")
321
+ if element.height and element.direction == "vertical":
322
+ styles.append(f"height: {format_style_value(element.height)}")
323
+ if element.borderRadius and element.borderRadius != "0":
324
+ styles.append(f"border-radius: {format_style_value(element.borderRadius)}")
325
+ if element.horizontalAlignment:
326
+ align_map = {
327
+ "left": "0",
328
+ "right": "auto",
329
+ "centered": "auto"
330
+ }
331
+ margin_left = align_map.get(element.horizontalAlignment, "auto")
332
+ margin_right = align_map.get(element.horizontalAlignment, "auto")
333
+ styles.append(f"margin-left: {margin_left}; margin-right: {margin_right}")
334
+
335
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
336
+ return f'{indent}<div class="line"{style_attr}></div>'
337
+
338
+ elif element_type == "spacer":
339
+ styles = []
340
+ if element.width:
341
+ styles.append(f"width: {format_style_value(element.width)}")
342
+ if element.height:
343
+ styles.append(f"height: {format_style_value(element.height)}")
344
+
345
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
346
+ return f'{indent}<div class="spacer"{style_attr}></div>'
347
+
348
+ elif element_type == "table":
349
+ styles = []
350
+ if element.width:
351
+ styles.append(f"width: {format_style_value(element.width)}")
352
+ if element.borderRadius and element.borderRadius != "0":
353
+ styles.append(f"border-radius: {format_style_value(element.borderRadius)}")
354
+
355
+ style_attr = f' style="{"; ".join(styles)}"' if styles else ""
356
+
357
+ columns = element.columns or [{"id": "col1", "title": "Coluna 1", "width": "100px"}]
358
+ rows = element.rows or [{"id": "row1"}]
359
+
360
+ html_parts = [f'{indent}<table class="table"{style_attr}>']
361
+
362
+ # Cabeçalho
363
+ html_parts.append(f'{indent} <thead>')
364
+ html_parts.append(f'{indent} <tr>')
365
+ for column in columns:
366
+ header_id = f'header-{column.get("id", "")}'
367
+ header_children = [e for e in elements_dict.values() if e.parentId == header_id]
368
+ header_style = f' style="background-color: {element.headerBackgroundColor};"' if element.headerBackgroundColor else ""
369
+ header_content = column.get("title", "")
370
+ if header_children:
371
+ header_content = "\n" + "\n".join([generate_element_html(child, elements_dict, level + 4) for child in header_children]) + f"\n{indent} "
372
+ else:
373
+ header_content = escape(header_content)
374
+ html_parts.append(f'{indent} <th{header_style}>{header_content}</th>')
375
+ html_parts.append(f'{indent} </tr>')
376
+ html_parts.append(f'{indent} </thead>')
377
+
378
+ # Corpo
379
+ html_parts.append(f'{indent} <tbody>')
380
+ for row in rows:
381
+ html_parts.append(f'{indent} <tr>')
382
+ for column in columns:
383
+ cell_id = f'{row.get("id", "")}-{column.get("id", "")}'
384
+ cell_children = [e for e in elements_dict.values() if e.parentId == cell_id]
385
+ cell_style = f' style="background-color: {element.cellBackgroundColor};"' if element.cellBackgroundColor else ""
386
+ cell_content = ""
387
+ if cell_children:
388
+ cell_content = "\n" + "\n".join([generate_element_html(child, elements_dict, level + 4) for child in cell_children]) + f"\n{indent} "
389
+ html_parts.append(f'{indent} <td{cell_style}>{cell_content}</td>')
390
+ html_parts.append(f'{indent} </tr>')
391
+ html_parts.append(f'{indent} </tbody>')
392
+ html_parts.append(f'{indent}</table>')
393
+
394
+ return "\n".join(html_parts)
395
+
396
+ return ""
397
+
398
+ def generate_css(orientation: str = "portrait") -> str:
399
+ """Gera CSS básico para o PDF"""
400
+ is_portrait = orientation == "portrait"
401
+ page_width = "210mm" if is_portrait else "297mm"
402
+ page_height = "297mm" if is_portrait else "210mm"
403
+
404
+ css = """* {
405
+ margin: 0;
406
+ padding: 0;
407
+ box-sizing: border-box;
408
+ }
409
+
410
+ html, body {
411
+ margin: 0;
412
+ padding: 0;
413
+ width: 100%;
414
+ height: 100%;
415
+ font-family: Arial, sans-serif;
416
+ background: #f5f5f5;
417
+ }
418
+
419
+ .a4-page {
420
+ width: """ + page_width + """;
421
+ height: """ + page_height + """;
422
+ margin: 0;
423
+ padding: 0;
424
+ background: white;
425
+ display: flex;
426
+ flex-direction: column;
427
+ overflow: hidden;
428
+ box-sizing: border-box;
429
+ }
430
+
431
+ .paper-content {
432
+ width: 100%;
433
+ height: 100%;
434
+ padding: 0;
435
+ display: flex;
436
+ position: relative;
437
+ overflow: hidden;
438
+ }
439
+
440
+ .column-element,
441
+ .column {
442
+ display: flex;
443
+ flex-direction: column;
444
+ width: 100%;
445
+ }
446
+
447
+ .row-element,
448
+ .row {
449
+ display: flex;
450
+ flex-direction: row;
451
+ width: 100%;
452
+ }
453
+
454
+ .container-element,
455
+ .container {
456
+ display: block;
457
+ }
458
+
459
+ .text-element,
460
+ .text {
461
+ display: block;
462
+ word-wrap: break-word;
463
+ }
464
+
465
+ .image-element,
466
+ .image {
467
+ display: block;
468
+ max-width: 100%;
469
+ height: auto;
470
+ }
471
+
472
+ .line-element,
473
+ .line {
474
+ display: block;
475
+ }
476
+
477
+ .spacer-element,
478
+ .spacer {
479
+ display: block;
480
+ }
481
+
482
+ .table-element,
483
+ .table {
484
+ width: 100%;
485
+ border-collapse: collapse;
486
+ }
487
+
488
+ .table th,
489
+ .table td {
490
+ padding: 8px;
491
+ border: 1px solid #ddd;
492
+ }
493
+ """
494
+ return css
495
+
496
  @app.get("/")
497
  def greet_json():
498
  return {"Hello": "World!"}
 
582
  }
583
  )
584
 
585
+ except Exception as e:
586
+ raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}")
587
+
588
+ @app.post("/generate-pdf-from-json")
589
+ async def generate_pdf_from_json(request: PDFFromJSONRequest):
590
+ """
591
+ Gera PDF a partir de uma estrutura JSON de elementos.
592
+ Esta rota é mais eficiente que /generate-pdf pois não requer envio de HTML completo.
593
+
594
+ Exemplo de uso:
595
+ {
596
+ "elements": [
597
+ {
598
+ "id": "a4-page",
599
+ "type": "a4",
600
+ "children": ["root-column"],
601
+ "backgroundColor": "#FFFFFF"
602
+ },
603
+ {
604
+ "id": "root-column",
605
+ "type": "column",
606
+ "parentId": "a4-page",
607
+ "children": ["text-1"]
608
+ },
609
+ {
610
+ "id": "text-1",
611
+ "type": "text",
612
+ "parentId": "root-column",
613
+ "content": "Olá, mundo!",
614
+ "fontSize": "24px",
615
+ "color": "#000000"
616
+ }
617
+ ],
618
+ "project": {
619
+ "name": "Meu Documento",
620
+ "orientation": "portrait"
621
+ }
622
+ }
623
+ """
624
+ try:
625
+ # Cria dicionário de elementos para acesso rápido
626
+ elements_dict = {elem.id: elem for elem in request.elements}
627
+
628
+ # Encontra o elemento raiz (a4-page)
629
+ a4_element = None
630
+ for elem in request.elements:
631
+ if elem.type == "a4":
632
+ a4_element = elem
633
+ break
634
+
635
+ if not a4_element:
636
+ raise HTTPException(status_code=400, detail="Elemento 'a4' não encontrado nos elementos")
637
+
638
+ # Gera HTML a partir dos elementos
639
+ body_content = generate_element_html(a4_element, elements_dict, 1)
640
+
641
+ # Gera CSS
642
+ orientation = request.project.orientation if request.project else "portrait"
643
+ css_content = generate_css(orientation)
644
+
645
+ # Monta HTML completo
646
+ project_name = request.project.name if request.project else "Documento A4"
647
+ html_content = f"""<!DOCTYPE html>
648
+ <html lang="pt-BR">
649
+ <head>
650
+ <meta charset="UTF-8">
651
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
652
+ <title>{escape(project_name)}</title>
653
+ <style>
654
+ {css_content}
655
+ </style>
656
+ </head>
657
+ <body>
658
+ {body_content}
659
+ </body>
660
+ </html>"""
661
+
662
+ # Gera o PDF usando WeasyPrint
663
+ pdf_buffer = BytesIO()
664
+
665
+ # Configura orientação e tamanho da página A4
666
+ if orientation == "landscape":
667
+ html_doc = HTML(string=html_content)
668
+ css_doc = CSS(string=f"""
669
+ @page {{
670
+ size: 297mm 210mm;
671
+ margin: 0;
672
+ padding: 0;
673
+ }}
674
+ html, body {{
675
+ margin: 0;
676
+ padding: 0;
677
+ width: 297mm;
678
+ height: 210mm;
679
+ }}
680
+ .a4-page {{
681
+ width: 297mm;
682
+ height: 210mm;
683
+ margin: 0;
684
+ padding: 0;
685
+ }}
686
+ {css_content}
687
+ """)
688
+ html_doc.write_pdf(pdf_buffer, stylesheets=[css_doc])
689
+ else:
690
+ # Portrait (padrão)
691
+ html_doc = HTML(string=html_content)
692
+ css_doc = CSS(string=f"""
693
+ @page {{
694
+ size: 210mm 297mm;
695
+ margin: 0;
696
+ padding: 0;
697
+ }}
698
+ html, body {{
699
+ margin: 0;
700
+ padding: 0;
701
+ width: 210mm;
702
+ height: 297mm;
703
+ }}
704
+ .a4-page {{
705
+ width: 210mm;
706
+ height: 297mm;
707
+ margin: 0;
708
+ padding: 0;
709
+ }}
710
+ {css_content}
711
+ """)
712
+ html_doc.write_pdf(pdf_buffer, stylesheets=[css_doc])
713
+
714
+ pdf_buffer.seek(0)
715
+ pdf_bytes = pdf_buffer.read()
716
+
717
+ # Retorna o PDF como resposta
718
+ filename = f"{project_name}.pdf" if request.project and request.project.name else "documento.pdf"
719
+ return Response(
720
+ content=pdf_bytes,
721
+ media_type="application/pdf",
722
+ headers={
723
+ "Content-Disposition": f'attachment; filename="{filename}"'
724
+ }
725
+ )
726
+
727
+ except HTTPException:
728
+ raise
729
  except Exception as e:
730
  raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}")