1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(続々々)SVG帳票

Last updated at Posted at 2024-06-18

昨年末より何回か続いた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)

これで画像問題は解決だ。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?