22from io import BytesIO
33from io import StringIO
44from iosanita .contenttypes import _
5+ from PIL import Image
56from plone import api
7+ from plone .memoize import forever
68from Products .Five .browser import BrowserView
79from weasyprint import HTML
8- from zExceptions import BadRequest
10+ from zExceptions import NotFound
911from zope .interface import implementer
1012from zope .publisher .interfaces import IPublishTraverse
1113
14+ import base64
1215import csv
16+ import imghdr
1317import importlib .resources
1418import logging
1519import re
2125fontools_logger .setLevel (logging .WARNING )
2226
2327
24- CONTENT_TYPES_MAPPING = {
25- "csv" : "text/comma-separated-values" ,
26- "pdf" : "application/pdf" ,
27- "html" : "text/html" ,
28- }
28+ @forever .memoize
29+ def image_to_html (input_string ):
30+ """
31+ Convert image data to a base64 string formatted for HTML.
32+
33+ Args:
34+ - input_string: The string containing the filename and base64 encoded image data.
35+
36+ Returns:
37+ - HTML.
38+ """
39+
40+ if not input_string :
41+ return ""
42+
43+ # Split the input string to extract the filename and base64 data
44+ parts = input_string .split (";" )
45+ datab64 = parts [1 ].split (":" )[1 ]
46+
47+ # Decode the image data from base64
48+ image_data = base64 .b64decode (datab64 )
49+
50+ if image_data [:5 ] == b"<?xml" :
51+ # https://github.com/Kozea/WeasyPrint/issues/75
52+ # anche se il ticket risulta chiuso gli svg non risultano correttamente gestiti
53+ # return image_data
54+ # return f'<img src="data:image/svg+xml;charset=utf-8;base64,{datab64}">'
55+ # XXX: se non si va decode/encode il b64 non risulta corretto (!)
56+ # return f'<img src="data:image/svg+xml;charset=utf-8;base64,{base64.b64encode(image_data).decode()}">'
57+ # weasyprint gli svg non li gestisce comunque correttamente
58+ return None
59+
60+ # Guess the image format
61+ image_format = imghdr .what (None , image_data )
62+
63+ if not image_format :
64+ # raise ValueError("Unable to determine image format")
65+ logger .warning ("site logo, unable to determine image format" )
66+ return ""
67+
68+ # Open the image from the decoded data
69+ img = Image .open (BytesIO (image_data ))
70+
71+ # Create a buffer to hold the image data
72+ buffered = BytesIO ()
73+ img .save (buffered , format = image_format )
74+
75+ # Encode the image data to base64
76+ img_base64 = base64 .b64encode (buffered .getvalue ()).decode ("utf-8" )
77+
78+ # Format the base64 string for HTML
79+ return f'<img class="logo" src="data:{ image_format } ;base64,{ img_base64 } ">'
2980
3081
3182class IExportViewTraverser (IPublishTraverse ):
@@ -50,6 +101,8 @@ class ExportViewDownload(BrowserView):
50101
51102 """
52103
104+ with_footer = True
105+
53106 def __init__ (self , context , request ):
54107 super ().__init__ (context , request )
55108 self .export_type = "csv"
@@ -65,7 +118,7 @@ def publishTraverse(self, request, name):
65118 def __call__ (self ):
66119 """ """
67120 if self .export_type not in ["csv" , "pdf" , "html" ]:
68- raise BadRequest (
121+ raise NotFound (
69122 api .portal .translate (
70123 _ (
71124 "invalid_export_type" ,
@@ -74,18 +127,21 @@ def __call__(self):
74127 )
75128 )
76129 )
77- self .set_headers ()
78130 data = self .get_data ()
79- if not data :
80- return ""
81- resp_data = ""
82131 if self .export_type == "csv" :
83- resp_data = self .get_csv (data )
132+ # default per locales di riferimento (perr l'encoding, al momento, lasciamo
133+ # il generico utf-8 con BOM che potrebbe funzionare per tutti,
134+ # MS Excel incluso)
135+ lang = api .portal .get_current_language (self .context )
136+ if lang == "it" :
137+ sep = ";"
138+ else :
139+ sep = ","
140+ return self .get_csv (data , sep = sep )
84141 elif self .export_type == "pdf" :
85- resp_data = self .get_pdf (data )
142+ return self .get_pdf (data )
86143 elif self .export_type == "html" :
87- resp_data = self .get_html_for_pdf (data )
88- return resp_data
144+ return self .get_html_for_pdf (data )
89145
90146 def get_filename (self ):
91147 """
@@ -94,37 +150,46 @@ def get_filename(self):
94150 now = datetime .now ().strftime ("%Y_%m_%d_%H_%M" )
95151 return f"export_{ now } .{ self .export_type } "
96152
97- def set_headers (self ):
98- """
99- Set the headers for the response.
100- """
101- if self .export_type in ["pdf" , "csv" ]:
102- self .request .response .setHeader (
103- "Content-Disposition" , f"attachment;filename={ self .get_filename ()} "
104- )
105- self .request .response .setHeader (
106- "Content-Type" , CONTENT_TYPES_MAPPING [self .export_type ]
107- )
108-
109- def get_csv (self , data ):
153+ def get_csv (self , data , encoding = "utf-8-sig" , sep = "," ):
110154 """
111155 Generate CSV data from the provided data.
112156 """
157+ # 1. Crea uno StringIO per il CSV
158+ csv_buffer = StringIO ()
159+ # 2. Aggiungi l'header per il separatore (specifico per Excel)
160+ # In Libreoffice viene aggiunta una riga, per ora evitiamo
161+ # csv_buffer.write(f"sep={sep}\n")
162+ # 3. Scrittura dei dati CSV
113163 columns = self .get_columns (data )
114-
115- csv_data = StringIO ()
116- csv_writer = csv .writer (csv_data , quoting = csv .QUOTE_ALL )
164+ csv_writer = csv .writer (csv_buffer , delimiter = sep , quoting = csv .QUOTE_ALL )
117165 csv_writer .writerow ([c ["title" ] for c in columns ])
118-
119166 for item in data :
120167 csv_writer .writerow (self .format_row (item ))
121- return csv_data .getvalue ().encode ("utf-8" )
168+ # 4. Prepara i bytes con BOM (UTF-8-sig)
169+ csv_data = csv_buffer .getvalue ()
170+ if encoding == "utf-8-sig" :
171+ csv_bytes = b"\xef \xbb \xbf " + csv_data .encode ("utf-8" ) # Aggiunge BOM
172+ else :
173+ csv_bytes = csv_data .encode (encoding )
174+ # 5. Crea la risposta con gli header corretti
175+ response = self .request .response
176+ response .setHeader (
177+ "Content-Disposition" , f"attachment;filename={ self .get_filename ()} "
178+ )
179+ response .setHeader ("Content-Type" , f"text/csv; charset={ encoding } " )
180+ return csv_bytes
122181
123182 def get_pdf (self , data ):
124183 html_str = self .get_html_for_pdf (data = data )
125184 pdf_file = BytesIO ()
126185 HTML (string = html_str ).write_pdf (pdf_file )
127186 pdf_file .seek (0 )
187+ # 5. Crea la risposta con gli header corretti
188+ response = self .request .response
189+ response .setHeader (
190+ "Content-Disposition" , f"attachment;filename={ self .get_filename ()} "
191+ )
192+ response .setHeader ("Content-Type" , "application/pdf" )
128193 return pdf_file .read ()
129194
130195 def get_data (self ):
@@ -176,6 +241,7 @@ def get_html_for_pdf(self, data):
176241 context = self ,
177242 request = self .request ,
178243 )
244+
179245 return view (rows = data , columns = columns )
180246
181247 def pdf_styles (self ):
@@ -184,7 +250,11 @@ def pdf_styles(self):
184250 )
185251
186252 def pdf_title (self ):
187- return None
253+ context = self .context .context
254+ site_title = api .portal .get_registry_record ("plone.site_title" )
255+ if site_title :
256+ return f"{ site_title } : { context .Title ()} "
257+ return context .Title ()
188258
189259 def pdf_description (self ):
190260 return None
@@ -203,3 +273,13 @@ def pdf_cell_format(self, column, value):
203273 if re .match (r"^\d{4}-\d{2}-\d{2}T00:00:00$" , value ):
204274 return {"type" : "str" , "value" : value .split ("T" )[0 ]}
205275 return {"type" : "str" , "value" : str (value )}
276+
277+ def pdf_logo (self ):
278+ site_logo = api .portal .get_registry_record ("plone.site_logo" )
279+ if site_logo :
280+ return image_to_html (site_logo .decode ())
281+ return None
282+
283+ def pdf_datetime (self ):
284+ # TODO: valutare localizzazione della data
285+ return datetime .now ().strftime ("%d/%m/%Y %H:%M" )
0 commit comments