From 2ca77b604b9b13b9d56b5cc6c222da991e75c0e8 Mon Sep 17 00:00:00 2001 From: hayano Date: Sat, 9 Nov 2024 09:50:58 +0000 Subject: [PATCH] Fix PDF issue --- Dockerfile.gdal | 30 +++++ SumasenLibs/excel_lib/sumaexcel/sumaexcel.py | 92 +++++++++++--- config/fonts.conf | 69 ++++++++++ rog/utils.py | 20 +-- rog/views.py | 126 +++++++++++++++++-- supervisor/html/index.html | 64 +--------- 6 files changed, 300 insertions(+), 101 deletions(-) create mode 100644 config/fonts.conf diff --git a/Dockerfile.gdal b/Dockerfile.gdal index 0fcd129..8d4f7ce 100644 --- a/Dockerfile.gdal +++ b/Dockerfile.gdal @@ -45,8 +45,36 @@ RUN apt-get update && \ libreoffice \ libreoffice-calc \ libreoffice-writer \ + libreoffice-java-common \ + fonts-ipafont \ + fonts-ipafont-gothic \ + fonts-ipafont-mincho \ + language-pack-ja \ + fontconfig \ + locales \ python3-uno # LibreOffice Python バインディング + +# 日本語ロケールの設定 +RUN locale-gen ja_JP.UTF-8 +ENV LANG=ja_JP.UTF-8 +ENV LC_ALL=ja_JP.UTF-8 +ENV LANGUAGE=ja_JP:ja + +# フォント設定ファイルをコピー +COPY config/fonts.conf /etc/fonts/local.conf + +# フォントキャッシュの更新 +RUN fc-cache -f -v + +# LibreOfficeの作業ディレクトリを作成 +RUN mkdir -p /var/cache/libreoffice && \ + chmod 777 /var/cache/libreoffice + +# フォント設定の権限を設定 +RUN chmod 644 /etc/fonts/local.conf + + # 作業ディレクトリとパーミッションの設定 RUN mkdir -p /app/docbase /tmp/libreoffice && \ chmod -R 777 /app/docbase /tmp/libreoffice @@ -66,6 +94,8 @@ RUN apt-get update COPY ./requirements.txt /app/requirements.txt +RUN pip install boto3==1.26.137 + # Install Gunicorn RUN pip install gunicorn diff --git a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py index 98cea28..b85ef32 100644 --- a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py +++ b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py @@ -305,6 +305,12 @@ class SumasenExcel: logging.error(f"Error in proceed_group: {str(e)}") return {"status": False, "message": f"Exception in proceed_group : Error generating report: {str(e)}"} + def pixels_to_EMU(pixels): + """ + Convert pixels to EMU (English Metric Units) + EMU = pixels * 9525 + """ + return int(pixels * 9525) def format_cell_value(self, field_value, cell): """セルの値を適切な形式に変換する @@ -369,7 +375,8 @@ class SumasenExcel: # ワークシートを取得 worksheet = cell.parent - + + logging.info('step-1') try: # 列の幅を取得(文字単位からピクセルに変換) column_letter = get_column_letter(cell.column) @@ -378,6 +385,7 @@ class SumasenExcel: except Exception: cell_width = 100 # デフォルト値 + logging.info('step-2') try: # 行の高さを取得(ポイント単位からピクセルに変換) row_height = worksheet.row_dimensions[cell.row].height @@ -385,6 +393,7 @@ class SumasenExcel: except Exception: cell_height = 20 # デフォルト値 + logging.info('step-3') # マージンの設定(ピクセル単位) margin_horizontal = 4 # 左右のマージン margin_vertical = 2 # 上下のマージン @@ -393,6 +402,7 @@ class SumasenExcel: effective_cell_width = cell_width - (margin_horizontal * 2) effective_cell_height = cell_height - (margin_vertical * 2) + logging.info('step-4') # 最小サイズを設定(マージンを考慮) effective_cell_width = max(effective_cell_width, 92) # 100 - (4 * 2) effective_cell_height = max(effective_cell_height, 16) # 20 - (2 * 2) @@ -403,6 +413,7 @@ class SumasenExcel: effective_cell_width = min(effective_cell_width, max_width) effective_cell_height = min(effective_cell_height, max_height) + logging.info('step-5') # アスペクト比を保持しながらリサイズ img_width, img_height = img.size img_aspect = img_width / img_height @@ -414,7 +425,11 @@ class SumasenExcel: else: height = effective_cell_height width = int(height * img_aspect) - + + # 画像処理部分の修正 + #from openpyxl.utils.units import pixels_to_EMU + + logging.info('step-6') # 画像をリサイズ img_resized = img.resize((width, height), Image.BICUBIC) @@ -422,29 +437,47 @@ class SumasenExcel: img_byte_arr = io.BytesIO() img_resized.save(img_byte_arr, format='JPEG', quality=85, optimize=True) img_byte_arr.seek(0) - - # OpenPyXLのImageオブジェクトを作成 - from openpyxl.drawing.image import Image as XLImage - excel_image = XLImage(img_byte_arr) - - # 画像のオフセット位置を設定(マージンを適用) - from openpyxl.drawing.spreadsheet_drawing import OneCellAnchor, AnchorMarker - from openpyxl.utils.units import pixels_to_EMU - # セルの左上を基準に、マージン分オフセットした位置に配置 - col_offset = pixels_to_EMU(margin_horizontal) - row_offset = pixels_to_EMU(margin_vertical) - - # マージンを考慮した配置 - marker = AnchorMarker(col=cell.column - 1, colOff=col_offset, - row=cell.row - 1, rowOff=row_offset) - anchor = OneCellAnchor(_from=marker, ext=None) + + logging.info('step-7') + # OpenPyXLのImageオブジェクトを作成 + excel_image = XLImage(img_byte_arr) + + # EMUユニットでのサイズを設定 + excel_image.width = pixels_to_EMU(width) + excel_image.height = pixels_to_EMU(height) + + # 正しいアンカー設定 + from openpyxl.drawing.spreadsheet_drawing import OneCellAnchor, AnchorMarker + from openpyxl.drawing.xdr import XDRPositiveSize2D + + logging.info('step-8') + marker = AnchorMarker( + col=cell.column - 1, + colOff=pixels_to_EMU(margin_horizontal), + row=cell.row - 1, + rowOff=pixels_to_EMU(margin_vertical) + ) + + # XDRPositiveSize2Dを使用して画像サイズを設定 + size = XDRPositiveSize2D( + cx=pixels_to_EMU(width), + cy=pixels_to_EMU(height) + ) + + anchor = OneCellAnchor(_from=marker, ext=size) excel_image.anchor = anchor - + + logging.info('step-9') # 画像をワークシートに追加 worksheet.add_image(excel_image) + #cell.parent.add_image(excel_image) + logging.info('step-A') + # メモリ解放 + #img_byte_arr.close() + return "" except Exception as e: @@ -467,7 +500,28 @@ class SumasenExcel: # その他の場合は文字列に変換 return str(field_value) + def verify_image_insertion(self, worksheet, cell, image): + """画像の挿入を検証するためのヘルパーメソッド""" + try: + # 画像が実際にワークシートに追加されているか確認 + images_in_sheet = worksheet._images + if not images_in_sheet: + logging.warning(f"No images found in worksheet at cell {cell.coordinate}") + return False + # 画像のアンカー位置を確認 + last_image = images_in_sheet[-1] + anchor = last_image.anchor + if not anchor: + logging.warning(f"Image anchor not set properly at cell {cell.coordinate}") + return False + + logging.info(f"Image successfully inserted at cell {cell.coordinate}") + return True + + except Exception as e: + logging.error(f"Error verifying image insertion: {str(e)}") + return False def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]): diff --git a/config/fonts.conf b/config/fonts.conf new file mode 100644 index 0000000..b6cac51 --- /dev/null +++ b/config/fonts.conf @@ -0,0 +1,69 @@ + + + + /usr/share/fonts + + + + + sans-serif + + + IPAexGothic + + + + + + + serif + + + IPAexMincho + + + + + + + MS Gothic + + + IPAexGothic + + + + + + + MS Mincho + + + IPAexMincho + + + + + + + false + + + + + + + hintslight + + + rgb + + + + + + + true + + + diff --git a/rog/utils.py b/rog/utils.py index 222d0f0..c2182ff 100644 --- a/rog/utils.py +++ b/rog/utils.py @@ -1,5 +1,5 @@ import os -from aiohttp import ClientError +from botocore.exceptions import ClientError from django.template.loader import render_to_string from django.conf import settings import logging @@ -193,12 +193,12 @@ class S3Bucket: s3_key = os.path.basename(file_path) # S3クライアントが指定されていない場合は新規作成 - if s3_client is None: - s3_client = self.connect() + if self.s3_client is None: + self.s3_client = self.connect() # ファイルのアップロード logger.info(f"アップロード開始: {file_path} → s3://{self.bucket_name}/{s3_key}") - s3_client.upload_file(file_path, self.bucket_name, s3_key) + self.s3_client.upload_file(file_path, self.bucket_name, s3_key) logger.info("アップロード完了") return True @@ -313,11 +313,11 @@ class S3Bucket: try: # S3クライアントが指定されていない場合は新規作成 - if s3_client is None: - s3_client = self.connect() + if self.s3_client is None: + self.s3_client = self.connect() # プレフィックスに一致するオブジェクトをリスト - paginator = s3_client.get_paginator('list_objects_v2') + paginator = self.s3_client.get_paginator('list_objects_v2') pages = paginator.paginate(Bucket=self.bucket_name, Prefix=prefix) for page in pages: @@ -361,12 +361,12 @@ class S3Bucket: try: # S3クライアントが指定されていない場合は新規作成 - if s3_client is None: - s3_client = self.connect() + if self.s3_client is None: + self.s3_client = self.connect() # オブジェクトの削除 logger.info(f"削除開始: s3://{self.bucket_name}/{s3_key}") - s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key) + self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key) logger.info("削除完了") return True diff --git a/rog/views.py b/rog/views.py index 2f9d6da..2fa76a4 100644 --- a/rog/views.py +++ b/rog/views.py @@ -2764,14 +2764,111 @@ def export_excel(request, zekken_number, event_code): status=status.HTTP_500_INTERNAL_SERVER_ERROR ) + + # 一時ディレクトリを作成(ASCII文字のみのパスを使用) + temp_dir = tempfile.mkdtemp(prefix='lo-') + logger.debug(f"Created temp directory: {temp_dir}") + + # ASCII文字のみの作業ディレクトリを作成 + work_dir = os.path.join(temp_dir, 'work') + output_dir = os.path.join(temp_dir, 'output') + os.makedirs(work_dir, exist_ok=True) + os.makedirs(output_dir, exist_ok=True) + + # すべてのディレクトリに適切な権限を設定 + for directory in [temp_dir, work_dir, output_dir]: + os.chmod(directory, 0o777) + logger.debug(f"Set permissions for directory: {directory}") + + # ASCII文字のみの一時ファイル名を使用 + temp_excel_name = f"certificate_{zekken_number}.xlsx" + temp_excel_path = os.path.join(work_dir, temp_excel_name) + + # 元のExcelファイルを作業ディレクトリにコピー + shutil.copy2(excel_path, temp_excel_path) + os.chmod(temp_excel_path, 0o666) + logger.debug(f"Copied Excel file to: {temp_excel_path}") + + + # LibreOffice設定ディレクトリを作成 + libreoffice_config_dir = os.path.join(temp_dir, 'libreoffice') + os.makedirs(libreoffice_config_dir, exist_ok=True) + + # フォント設定ディレクトリを作成 + font_conf_dir = os.path.join(temp_dir, 'fonts') + os.makedirs(font_conf_dir, exist_ok=True) + + # フォント設定ファイルを作成 + fonts_conf_content = ''' + + + /usr/share/fonts + + + sans-serif + + + IPAexGothic + + + + + serif + + + IPAexMincho + + +''' + + font_conf_path = os.path.join(libreoffice_config_dir, 'fonts.conf') + with open(font_conf_path, 'w') as f: + f.write(fonts_conf_content) + + + # LibreOfficeのフォント設定を作成 + registry_dir = os.path.join(libreoffice_config_dir, 'registry') + os.makedirs(registry_dir, exist_ok=True) + + # フォント埋め込み設定を作成 + pdf_export_config = ''' + + + + + true + + + true + + + true + + + +''' + + pdf_config_path = os.path.join(registry_dir, 'pdf_export.xcu') + with open(pdf_config_path, 'w') as f: + f.write(pdf_export_config) + + # すべてのディレクトリに適切な権限を設定 + for directory in [temp_dir, work_dir, output_dir,registry_dir]: + os.chmod(directory, 0o777) + logger.debug(f"Set permissions for directory: {directory}") + + os.chmod(temp_excel_path, 0o666) + os.chmod(font_conf_path, 0o666) + os.chmod(pdf_config_path, 0o666) + # フォーマット指定(excel or pdf) format_type = request.query_params.get('format', 'pdf') if format_type.lower() == 'pdf': try: # パスとファイル名に分離 - file_dir = os.path.dirname(excel_path) # パス部分の取得 - file_name = os.path.basename(excel_path) # ファイル名部分の取得 + file_dir = os.path.dirname(temp_excel_path) # パス部分の取得 + file_name = os.path.basename(temp_excel_path) # ファイル名部分の取得 # ファイル名の拡張子をpdfに変更 base_name = os.path.splitext(file_name)[0] # 拡張子を除いたファイル名 @@ -2786,11 +2883,10 @@ def export_excel(request, zekken_number, event_code): conversion_command = [ 'soffice', # LibreOfficeの代替コマンド '--headless', - '--convert-to', - 'pdf', - '--outdir', - file_dir, - excel_path + '--convert-to', 'pdf:writer_pdf_Export', + '--outdir',file_dir, + '-env:UserInstallation=file://' + libreoffice_config_dir, + temp_excel_path ] logger.debug(f"Running conversion command: {' '.join(conversion_command)}") @@ -2798,6 +2894,11 @@ def export_excel(request, zekken_number, event_code): # 環境変数を設定 env = os.environ.copy() #env['HOME'] = temp_dir # LibreOfficeの設定ディレクトリを一時ディレクトリに設定 + env['HOME'] = temp_dir + env['LANG'] = 'ja_JP.UTF-8' # 日本語環境を設定 + env['LC_ALL'] = 'ja_JP.UTF-8' + env['FONTCONFIG_FILE'] = font_conf_path + env['FONTCONFIG_PATH'] = font_conf_dir # 変換プロセスを実行 process = subprocess.run( @@ -2805,7 +2906,9 @@ def export_excel(request, zekken_number, event_code): env=env, capture_output=True, text=True, - check=True + cwd=work_dir, + check=True, + timeout=30 ) logger.debug(f"Conversion output: {process.stdout}") @@ -2819,9 +2922,10 @@ def export_excel(request, zekken_number, event_code): ) s3 = S3Bucket('sumasenrogaining') - s3.upload_file(pdf_path, f'{event_code}/scoreboard', f'certificate_{zekken_number}.pdf') - s3.upload_file(excel_path, f'{event_code}/scoreboard_excel', f'certificate_{zekken_number}.xlsx') + s3.upload_file(pdf_path, f'{event_code}/scoreboard/certificate_{zekken_number}.pdf') + s3.upload_file(excel_path, f'{event_code}/scoreboard_excel/certificate_{zekken_number}.xlsx') + os.remove(temp_excel_path) os.remove(excel_path) os.remove(pdf_path) @@ -2833,7 +2937,7 @@ def export_excel(request, zekken_number, event_code): return Response( {"error": f"PDF conversion failed: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) + ) finally: # 一時ディレクトリの削除 if temp_dir and os.path.exists(temp_dir): diff --git a/supervisor/html/index.html b/supervisor/html/index.html index 59fd871..fcfebb7 100755 --- a/supervisor/html/index.html +++ b/supervisor/html/index.html @@ -1196,67 +1196,9 @@ async function saveGoalTime(goalTimeStr, zekkenNumber, eventCode) { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - - // Blobとしてレスポンスを取得 - const blob = await response.blob(); - - // BlobをURLに変換 - const url = window.URL.createObjectURL(blob); - - // 印刷方法の選択を提供する関数 - const printPDF = () => { - // IEとその他のブラウザで異なる処理を行う - if (window.navigator.msSaveOrOpenBlob) { - // IEの場合 - window.navigator.msSaveOrOpenBlob(blob, `通過証明書_${zekkenNumber}_${eventCode}.pdf`); - } else { - // その他のブラウザの場合 - // iframeを作成して印刷用のコンテナとして使用 - const printFrame = document.createElement('iframe'); - printFrame.style.display = 'none'; - printFrame.src = url; - - printFrame.onload = () => { - try { - // iframe内のPDFを印刷 - printFrame.contentWindow.print(); - } catch (error) { - console.error('印刷プロセス中にエラーが発生しました:', error); - // 印刷に失敗した場合、新しいタブでPDFを開く - window.open(url, '_blank'); - } finally { - // 少し遅延してからクリーンアップ - setTimeout(() => { - document.body.removeChild(printFrame); - window.URL.revokeObjectURL(url); - }, 1000); - } - }; - - document.body.appendChild(printFrame); - } - }; - - // 確認ダイアログを表示 - const userChoice = window.confirm('PDFを印刷しますか?\n「キャンセル」を選択すると保存できます。'); - - if (userChoice) { - // 印刷を実行 - printPDF(); - } else { - // PDFを保存 - const a = document.createElement('a'); - a.href = url; - a.download = `通過証明書_${zekkenNumber}_${eventCode}.pdf`; - document.body.appendChild(a); - a.click(); - - // クリーンアップ - setTimeout(() => { - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }, 100); - } + // 確認ダイアログを表示 + const userChoice = window.confirm('PDFを印刷に回しました。'); + return } catch (error) { console.error('エクスポート中にエラーが発生しました:', error);