Fix PDF issue

This commit is contained in:
hayano
2024-11-09 09:50:58 +00:00
parent 27aed10a4a
commit 2ca77b604b
6 changed files with 300 additions and 101 deletions

View File

@ -45,8 +45,36 @@ RUN apt-get update && \
libreoffice \ libreoffice \
libreoffice-calc \ libreoffice-calc \
libreoffice-writer \ libreoffice-writer \
libreoffice-java-common \
fonts-ipafont \
fonts-ipafont-gothic \
fonts-ipafont-mincho \
language-pack-ja \
fontconfig \
locales \
python3-uno # LibreOffice Python バインディング 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 && \ RUN mkdir -p /app/docbase /tmp/libreoffice && \
chmod -R 777 /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 COPY ./requirements.txt /app/requirements.txt
RUN pip install boto3==1.26.137
# Install Gunicorn # Install Gunicorn
RUN pip install gunicorn RUN pip install gunicorn

View File

@ -305,6 +305,12 @@ class SumasenExcel:
logging.error(f"Error in proceed_group: {str(e)}") logging.error(f"Error in proceed_group: {str(e)}")
return {"status": False, "message": f"Exception in proceed_group : Error generating report: {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): def format_cell_value(self, field_value, cell):
"""セルの値を適切な形式に変換する """セルの値を適切な形式に変換する
@ -370,6 +376,7 @@ class SumasenExcel:
# ワークシートを取得 # ワークシートを取得
worksheet = cell.parent worksheet = cell.parent
logging.info('step-1')
try: try:
# 列の幅を取得(文字単位からピクセルに変換) # 列の幅を取得(文字単位からピクセルに変換)
column_letter = get_column_letter(cell.column) column_letter = get_column_letter(cell.column)
@ -378,6 +385,7 @@ class SumasenExcel:
except Exception: except Exception:
cell_width = 100 # デフォルト値 cell_width = 100 # デフォルト値
logging.info('step-2')
try: try:
# 行の高さを取得(ポイント単位からピクセルに変換) # 行の高さを取得(ポイント単位からピクセルに変換)
row_height = worksheet.row_dimensions[cell.row].height row_height = worksheet.row_dimensions[cell.row].height
@ -385,6 +393,7 @@ class SumasenExcel:
except Exception: except Exception:
cell_height = 20 # デフォルト値 cell_height = 20 # デフォルト値
logging.info('step-3')
# マージンの設定(ピクセル単位) # マージンの設定(ピクセル単位)
margin_horizontal = 4 # 左右のマージン margin_horizontal = 4 # 左右のマージン
margin_vertical = 2 # 上下のマージン margin_vertical = 2 # 上下のマージン
@ -393,6 +402,7 @@ class SumasenExcel:
effective_cell_width = cell_width - (margin_horizontal * 2) effective_cell_width = cell_width - (margin_horizontal * 2)
effective_cell_height = cell_height - (margin_vertical * 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_width = max(effective_cell_width, 92) # 100 - (4 * 2)
effective_cell_height = max(effective_cell_height, 16) # 20 - (2 * 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_width = min(effective_cell_width, max_width)
effective_cell_height = min(effective_cell_height, max_height) effective_cell_height = min(effective_cell_height, max_height)
logging.info('step-5')
# アスペクト比を保持しながらリサイズ # アスペクト比を保持しながらリサイズ
img_width, img_height = img.size img_width, img_height = img.size
img_aspect = img_width / img_height img_aspect = img_width / img_height
@ -415,6 +426,10 @@ class SumasenExcel:
height = effective_cell_height height = effective_cell_height
width = int(height * img_aspect) 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) img_resized = img.resize((width, height), Image.BICUBIC)
@ -423,27 +438,45 @@ class SumasenExcel:
img_resized.save(img_byte_arr, format='JPEG', quality=85, optimize=True) img_resized.save(img_byte_arr, format='JPEG', quality=85, optimize=True)
img_byte_arr.seek(0) img_byte_arr.seek(0)
logging.info('step-7')
# OpenPyXLのImageオブジェクトを作成 # OpenPyXLのImageオブジェクトを作成
from openpyxl.drawing.image import Image as XLImage
excel_image = XLImage(img_byte_arr) 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.spreadsheet_drawing import OneCellAnchor, AnchorMarker
from openpyxl.utils.units import pixels_to_EMU from openpyxl.drawing.xdr import XDRPositiveSize2D
# セルの左上を基準に、マージン分オフセットした位置に配置 logging.info('step-8')
col_offset = pixels_to_EMU(margin_horizontal) marker = AnchorMarker(
row_offset = pixels_to_EMU(margin_vertical) col=cell.column - 1,
colOff=pixels_to_EMU(margin_horizontal),
row=cell.row - 1,
rowOff=pixels_to_EMU(margin_vertical)
)
# マージンを考慮した配置 # XDRPositiveSize2Dを使用して画像サイズを設定
marker = AnchorMarker(col=cell.column - 1, colOff=col_offset, size = XDRPositiveSize2D(
row=cell.row - 1, rowOff=row_offset) cx=pixels_to_EMU(width),
anchor = OneCellAnchor(_from=marker, ext=None) cy=pixels_to_EMU(height)
)
anchor = OneCellAnchor(_from=marker, ext=size)
excel_image.anchor = anchor excel_image.anchor = anchor
logging.info('step-9')
# 画像をワークシートに追加 # 画像をワークシートに追加
worksheet.add_image(excel_image) worksheet.add_image(excel_image)
#cell.parent.add_image(excel_image)
logging.info('step-A')
# メモリ解放
#img_byte_arr.close()
return "" return ""
@ -467,7 +500,28 @@ class SumasenExcel:
# その他の場合は文字列に変換 # その他の場合は文字列に変換
return str(field_value) 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]): def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]):

69
config/fonts.conf Normal file
View File

@ -0,0 +1,69 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/usr/share/fonts</dir>
<!-- デフォルトのサンセリフフォントをIPAexGothicに設定 -->
<match target="pattern">
<test qual="any" name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexGothic</string>
</edit>
</match>
<!-- デフォルトのセリフフォントをIPAexMinchoに設定 -->
<match target="pattern">
<test qual="any" name="family">
<string>serif</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexMincho</string>
</edit>
</match>
<!-- MS Gothic の代替としてIPAexGothicを使用 -->
<match target="pattern">
<test name="family">
<string>MS Gothic</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexGothic</string>
</edit>
</match>
<!-- MS Mincho の代替としてIPAexMinchoを使用 -->
<match target="pattern">
<test name="family">
<string>MS Mincho</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexMincho</string>
</edit>
</match>
<!-- ビットマップフォントを無効化 -->
<match target="font">
<edit name="embeddedbitmap" mode="assign">
<bool>false</bool>
</edit>
</match>
<!-- フォントのヒンティング設定 -->
<match target="font">
<edit name="hintstyle" mode="assign">
<const>hintslight</const>
</edit>
<edit name="rgba" mode="assign">
<const>rgb</const>
</edit>
</match>
<!-- アンチエイリアス設定 -->
<match target="font">
<edit name="antialias" mode="assign">
<bool>true</bool>
</edit>
</match>
</fontconfig>

View File

@ -1,5 +1,5 @@
import os import os
from aiohttp import ClientError from botocore.exceptions import ClientError
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.conf import settings from django.conf import settings
import logging import logging
@ -193,12 +193,12 @@ class S3Bucket:
s3_key = os.path.basename(file_path) s3_key = os.path.basename(file_path)
# S3クライアントが指定されていない場合は新規作成 # S3クライアントが指定されていない場合は新規作成
if s3_client is None: if self.s3_client is None:
s3_client = self.connect() self.s3_client = self.connect()
# ファイルのアップロード # ファイルのアップロード
logger.info(f"アップロード開始: {file_path} → s3://{self.bucket_name}/{s3_key}") 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("アップロード完了") logger.info("アップロード完了")
return True return True
@ -313,11 +313,11 @@ class S3Bucket:
try: try:
# S3クライアントが指定されていない場合は新規作成 # S3クライアントが指定されていない場合は新規作成
if s3_client is None: if self.s3_client is None:
s3_client = self.connect() 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) pages = paginator.paginate(Bucket=self.bucket_name, Prefix=prefix)
for page in pages: for page in pages:
@ -361,12 +361,12 @@ class S3Bucket:
try: try:
# S3クライアントが指定されていない場合は新規作成 # S3クライアントが指定されていない場合は新規作成
if s3_client is None: if self.s3_client is None:
s3_client = self.connect() self.s3_client = self.connect()
# オブジェクトの削除 # オブジェクトの削除
logger.info(f"削除開始: s3://{self.bucket_name}/{s3_key}") 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("削除完了") logger.info("削除完了")
return True return True

View File

@ -2764,14 +2764,111 @@ def export_excel(request, zekken_number, event_code):
status=status.HTTP_500_INTERNAL_SERVER_ERROR 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 = '''<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/usr/share/fonts</dir>
<match target="pattern">
<test qual="any" name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexGothic</string>
</edit>
</match>
<match target="pattern">
<test qual="any" name="family">
<string>serif</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>IPAexMincho</string>
</edit>
</match>
</fontconfig>'''
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 = '''<?xml version="1.0" encoding="UTF-8"?>
<oor:data xmlns:oor="http://openoffice.org/2001/registry">
<oor:component-data oor:package="org.openoffice.Office.Common" oor:name="Filter">
<node oor:name="PDF">
<prop oor:name="EmbedFonts" oor:op="fuse">
<value>true</value>
</prop>
<prop oor:name="ExportFormFields" oor:op="fuse">
<value>true</value>
</prop>
<prop oor:name="UseTaggedPDF" oor:op="fuse">
<value>true</value>
</prop>
</node>
</oor:component-data>
</oor:data>'''
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 # フォーマット指定excel or pdf
format_type = request.query_params.get('format', 'pdf') format_type = request.query_params.get('format', 'pdf')
if format_type.lower() == 'pdf': if format_type.lower() == 'pdf':
try: try:
# パスとファイル名に分離 # パスとファイル名に分離
file_dir = os.path.dirname(excel_path) # パス部分の取得 file_dir = os.path.dirname(temp_excel_path) # パス部分の取得
file_name = os.path.basename(excel_path) # ファイル名部分の取得 file_name = os.path.basename(temp_excel_path) # ファイル名部分の取得
# ファイル名の拡張子をpdfに変更 # ファイル名の拡張子をpdfに変更
base_name = os.path.splitext(file_name)[0] # 拡張子を除いたファイル名 base_name = os.path.splitext(file_name)[0] # 拡張子を除いたファイル名
@ -2786,11 +2883,10 @@ def export_excel(request, zekken_number, event_code):
conversion_command = [ conversion_command = [
'soffice', # LibreOfficeの代替コマンド 'soffice', # LibreOfficeの代替コマンド
'--headless', '--headless',
'--convert-to', '--convert-to', 'pdf:writer_pdf_Export',
'pdf', '--outdir',file_dir,
'--outdir', '-env:UserInstallation=file://' + libreoffice_config_dir,
file_dir, temp_excel_path
excel_path
] ]
logger.debug(f"Running conversion command: {' '.join(conversion_command)}") 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 = os.environ.copy()
#env['HOME'] = temp_dir # LibreOfficeの設定ディレクトリを一時ディレクトリに設定 #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( process = subprocess.run(
@ -2805,7 +2906,9 @@ def export_excel(request, zekken_number, event_code):
env=env, env=env,
capture_output=True, capture_output=True,
text=True, text=True,
check=True cwd=work_dir,
check=True,
timeout=30
) )
logger.debug(f"Conversion output: {process.stdout}") logger.debug(f"Conversion output: {process.stdout}")
@ -2819,9 +2922,10 @@ def export_excel(request, zekken_number, event_code):
) )
s3 = S3Bucket('sumasenrogaining') s3 = S3Bucket('sumasenrogaining')
s3.upload_file(pdf_path, f'{event_code}/scoreboard', f'certificate_{zekken_number}.pdf') s3.upload_file(pdf_path, f'{event_code}/scoreboard/certificate_{zekken_number}.pdf')
s3.upload_file(excel_path, f'{event_code}/scoreboard_excel', f'certificate_{zekken_number}.xlsx') 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(excel_path)
os.remove(pdf_path) os.remove(pdf_path)
@ -2833,7 +2937,7 @@ def export_excel(request, zekken_number, event_code):
return Response( return Response(
{"error": f"PDF conversion failed: {str(e)}"}, {"error": f"PDF conversion failed: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
finally: finally:
# 一時ディレクトリの削除 # 一時ディレクトリの削除
if temp_dir and os.path.exists(temp_dir): if temp_dir and os.path.exists(temp_dir):

View File

@ -1196,67 +1196,9 @@ async function saveGoalTime(goalTimeStr, zekkenNumber, eventCode) {
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
// 確認ダイアログを表示
// Blobとしてレスポンスを取得 const userChoice = window.confirm('PDFを印刷に回しました。');
const blob = await response.blob(); return
// 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);
}
} catch (error) { } catch (error) {
console.error('エクスポート中にエラーが発生しました:', error); console.error('エクスポート中にエラーが発生しました:', error);