Update app.py
Browse files
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)}")
|