昨年末より何回か続いたSVG帳票シリーズ(前回続々SVG帳票)だが大事なことを後回しにしていた。画像問題である。きっかけになった以下の記事で軽く触れられているがBase64データをプレースホルダーとして置換するというちょっとアレな方法。
SVGの画像は埋め込みが基本でリンクは非推奨だ。実際PDF変換ツールをいくつか試してみたがリンクでは変換できないし、埋め込みも容量が大きくなると失敗する。これじゃ画像を扱えないじゃないか。
そこで考えた。SVGから一気にPDFにしようと思うからできないのであって、画像なしのPDFとリンク画像のPDFを用意してマージすればよいのではないか。試行錯誤の末(今回もClaude 3に大変お世話になったが)できた。
FigmaでエクスポートしたSVGの画像部分は以下の感じになっているのでimage要素のxlink:hrefを手動でプレースホルダーに書き換えておく(この例では{image1_placeholder}にしてます)。
<svg width="595" height="420" viewBox="0 0 595 420" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="595" height="420" fill="#F5F5F5"/>
<g id="certificate">
<rect id="image 1" x="440" y="55" width="100" height="100" fill="url(#pattern0_0_1)"/>
<rect id="image 2" x="498" y="284" width="60" height="60" fill="url(#pattern1_0_1)"/>
</g>
<defs>
<pattern id="pattern0_0_1" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_0_1" transform="scale(0.0078125)"/>
</pattern>
<pattern id="pattern1_0_1" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image1_0_1" transform="scale(0.0166667)"/>
</pattern>
<image id="image0_0_1" data-name="french-bulldog-silhouette_gray_x128.png" width="128" height="128" xlink:href="{image1_placeholder}"/>
<image id="image1_0_1" data-name="Logo_.png" width="60" height="60" xlink:href="{image2_placeholder}"/>
</defs>
これをVueで実際の画像URLに置換してAPIサーバーに投げてPDFを返してもらう。サーバー側は以下の感じ。
UPLOAD_DIR = "./files"
def get_pdf(db: Session, request: schemas.File):
try:
pdf_files = []
for i, svg in enumerate(reversed(request.file)):
svg_file = os.path.join(UPLOAD_DIR, str(i).zfill(3) + "-" + request.name + ".svg")
with open(svg_file, 'w') as f:
f.write(svg)
# 背景PDFを作成
background_pdf_file = os.path.join(UPLOAD_DIR, str(i).zfill(3) + "-" + request.name + "_background.pdf")
subprocess.run("rsvg-convert -f pdf -o " + background_pdf_file + " " + svg_file, shell=True)
# 画像のリンク情報を抽出
image_links = extract_image_links(svg_file)
# 画像をPDFに変換
image_pdf_file = os.path.join(UPLOAD_DIR, str(i).zfill(3) + "-" + request.name + "_images.pdf")
convert_images_to_pdf(svg_file, image_links, image_pdf_file, request.name)
# レイヤーを結合
merged_pdf_file = os.path.join(UPLOAD_DIR, str(i).zfill(3) + "-" + request.name + "_merged.pdf")
merge_pdfs_as_layers(background_pdf_file, image_pdf_file, merged_pdf_file)
pdf_files.append(merged_pdf_file)
# 複数のPDFを1つにマージ
final_pdf_file = os.path.join(UPLOAD_DIR, request.name + ".pdf")
merge_pdfs(pdf_files, final_pdf_file)
# 一時ファイルを削除
subprocess.run(f"rm -f {os.path.join(UPLOAD_DIR, '*' + request.name + '*.svg')}", shell=True)
subprocess.run(f"rm -f {os.path.join(UPLOAD_DIR, '*' + request.name + '_*')}", shell=True)
filename = request.name + ".pdf"
response = FileResponse(
path=final_pdf_file,
filename=filename
)
return response
except Exception as e:
logging.error(f"get_pdf error: {str(e)}")
raise e
def extract_image_links(svg_file):
tree = ET.parse(svg_file)
root = tree.getroot()
image_links = []
for elem in root.iter("{http://www.w3.org/2000/svg}image"):
image_link = elem.get("{http://www.w3.org/1999/xlink}href")
image_id = elem.get("id")
if image_link:
image_id = image_id.replace("image", "") # idからimage部分を取り除く(0_0_1部分を取得)
image_links.append((image_id, image_link))
logging.debug(f"Extracted image links from {svg_file}: {image_links}")
return image_links
def convert_images_to_pdf(svg_file, image_links, output_file, file_prefix):
try:
# SVGファイルを解析する
tree = ET.parse(svg_file)
root = tree.getroot()
# 用紙サイズを取得する
width = float(root.attrib['width'])
height = float(root.attrib['height'])
# 新しいPDFを作成する
c = canvas.Canvas(output_file, pagesize=(width, height))
for i, (image_id, link) in enumerate(image_links):
# Parse the URL
parsed_url = urllib.parse.urlparse(link)
# Extract the file name from the parsed URL path
file_name = os.path.basename(parsed_url.path)
# Decode the URL-encoded file name
decoded_file_name = urllib.parse.unquote(file_name)
# Extract the file extension from the decoded file name
file_extension = os.path.splitext(decoded_file_name)[1]
# Generate a new file name using the file prefix, index, and extension
new_file_name = f"{file_prefix}_image{i}{file_extension}"
# Construct the file path
file_path = os.path.join(UPLOAD_DIR, new_file_name)
# Download the image and save it locally
response = requests.get(link)
with open(file_path, "wb") as f:
f.write(response.content)
# 画像を読み込む
image = Image.open(file_path)
logging.debug(f"Downloaded image: {file_path}")
logging.debug(f"Image size: {image.size}")
# 画像を表示すべき位置とサイズを取得
rect_elem = None
for elem in root.findall(".//svg:rect", namespaces={'svg': 'http://www.w3.org/2000/svg'}):
fill_attr = elem.get("fill")
if fill_attr:
fill_id = fill_attr.replace("url(#pattern", "").replace(")", "")
if fill_id == image_id:
rect_elem = elem
break
if rect_elem is not None:
x = float(rect_elem.attrib.get('x', '0'))
y = float(rect_elem.attrib.get('y', '0'))
rect_width = float(rect_elem.attrib.get('width', '0'))
rect_height = float(rect_elem.attrib.get('height', '0'))
logging.debug(f"Rect element: {x, y, rect_width, rect_height}")
# 画像のリサイズと配置の処理
# 一時画像ファイルの縦横のサイズから縦横比を計算する
img_width, img_height = image.size
aspect_ratio = img_width / img_height
# 画像のサイズを計算する
if aspect_ratio > rect_width / rect_height:
# 横幅に合わせる
new_width = rect_width
new_height = new_width / aspect_ratio
else:
# 高さに合わせる
new_height = rect_height
new_width = new_height * aspect_ratio
# 中央の位置を計算する
center_x = x + rect_width / 2
center_y = height - (y + rect_height / 2)
# 画像をリサイズする
resized_image = image.resize((int(new_width), int(new_height)))
logging.debug(f"Resized image size: {resized_image.size}")
logging.debug(f"Placed image at ({center_x - new_width / 2}, {center_y - new_height / 2})")
# リサイズした画像をPDFに配置する
c.drawImage(ImageReader(resized_image), center_x - new_width / 2, center_y - new_height / 2)
else:
logging.debug(f"Could not find rect element with fill containing 'url(#{pattern_id})'")
# SVG内の全てのrect要素を取得してデバッグ情報として出力
rect_elems = root.findall(".//svg:rect", namespaces={'svg': 'http://www.w3.org/2000/svg'})
logging.debug(f"All rect elements in the SVG:")
for rect in rect_elems:
logging.debug(f" - {rect.attrib}")
# PDFを保存する
c.showPage()
c.save()
except Exception as e:
logging.error(f"convert_images_to_pdf error: {str(e)}")
raise e
def merge_pdfs_as_layers(background_pdf, foreground_pdf, output_pdf):
try:
# 背景PDFを読み込む
background = PdfReader(open(background_pdf, 'rb'))
# 前景PDFを読み込む
foreground = PdfReader(open(foreground_pdf, 'rb'))
# 出力用のPDFWriterを作成
output = PdfWriter()
# 背景PDFの最初のページを取得
page = background.pages[0]
# 前景PDFの最初のページを取得
overlay_page = foreground.pages[0]
# 背景ページの上に前景ページを重ねる
page.merge_page(overlay_page)
# 結合したページを出力PDFに追加
output.add_page(page)
# 出力PDFを保存
with open(output_pdf, 'wb') as f:
output.write(f)
except Exception as e:
logging.error(f"merge_pdfs_as_layers error: {str(e)}")
raise e
def merge_pdfs(pdf_files, output_pdf):
merger = PyPDF2.PdfMerger()
for pdf in reversed(pdf_files):
with open(pdf, 'rb') as file:
reader = PyPDF2.PdfReader(file)
merger.append(reader)
with open(output_pdf, 'wb') as file:
merger.write(file)
これで画像問題は解決だ。