# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/pdf.ipynb.

# %% auto 0
__all__ = ['PdfClient']

# %% ../nbs/pdf.ipynb 3
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.lib.pagesizes import A4, landscape
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
import hashlib
import os
import json
import requests
from PIL import Image
from reportlab.lib.colors import *
from tqdm import tqdm
from reportlab.lib import colors
import random
from bs4 import BeautifulSoup
from typing import Union, Dict, List, Any

# %% ../nbs/pdf.ipynb 4
class PdfClient:
    """PDFを操作するクラス"""

    tmp_dir = "tmp"
    debug = False

    def __init__(self):
        """
        コンストラクタ。一時フォルダを作成します。
        """
        os.makedirs(self.tmp_dir, exist_ok=True)

    def get_manifest_json_from_path(self, path):
        """
        ファイルパスからJSONデータを読み込みます。

        Parameters
        ----------
        path : str
            JSONファイルのパス

        Returns
        -------
        dict
            JSONファイルから読み込んだデータ
        """
        with open(path) as f:
            manifest_json = json.load(f)
        return manifest_json

    def get_manifest_json_from_url(self, iiif_manifest_url):
        """
        URLからJSONデータを取得します。すでに取得済みの場合はそのデータを返します。

        Parameters
        ----------
        iiif_manifest_url : str
            JSONデータのURL

        Returns
        -------
        dict
            URLから取得したJSONデータ
        """
        hs = hashlib.md5(iiif_manifest_url.encode()).hexdigest()
        path = f"{self.tmp_dir}/manifest/{hs}.json"

        if not os.path.exists(path):
            os.makedirs(os.path.dirname(path), exist_ok=True)
            df = requests.get(iiif_manifest_url).json()
            json.dump(df, open(path, "w"), ensure_ascii=False, indent=4)

        return self.get_manifest_json_from_path(path)

    def getHiImagePath(self, image_url):
        """
        画像のURLから対応するTIFF画像のパスを取得します。

        Parameters
        ----------
        image_url : str
            画像のURL。例: "https://clioimg.hi.u-tokyo.ac.jp/viewer/api/image/idata/850/8500/58/0201/0001.jpg"

        Returns
        -------
        img_path : str
            画像のローカルパス。画像が既に存在しない場合、ダウンロードします。

        """
        # URLから不要な部分を取り除きます
        tmp = image_url.replace("https://clioimg.hi.u-tokyo.ac.jp/viewer/api/image/idata", "").replace(".jpg", "")
        spl = tmp.split("%2F")
        base_image_url = "https://clioimg.hi.u-tokyo.ac.jp/viewer/image/idata"

        # 画像のURLを構築します
        image_url = "/".join([base_image_url] + [t for t in spl if t]) + ".tif"

        # 画像URLからハッシュを作成し、画像の保存パスを取得します
        image_hash = hashlib.md5(image_url.encode()).hexdigest()
        img_path = f"{self.task_tmp_dir}/images/{image_hash}.tif"

        # 画像が存在しない場合、ダウンロードして保存します
        if not os.path.exists(img_path):
            os.makedirs(os.path.dirname(img_path), exist_ok=True)
            content = requests.get(image_url).content
            with open(img_path, "wb") as file:
                file.write(content)

        return img_path

    def set_image(self, newPdfPage, im, img_path, page_size):
        """
        画像をPDFページに配置し、必要に応じて圧縮します。

        Parameters
        ----------
        newPdfPage : PDF Page Object
            画像を配置するPDFページ。
        im : Image Object
            配置する画像オブジェクト。
        img_path : str
            画像の保存パス。
        page_size : dict
            ページサイズの情報。keys: 'width', 'height'

        """
        # 画像の圧縮が必要な場合、圧縮して保存します
        if self.is_compressed:
            im_jpg_path = img_path + "_resized.jpg"
            im.save(im_jpg_path, optimize=True, quality=self.compress_quality)
        else:
            im_jpg_path = img_path

        # 画像をPDFページに描画します
        newPdfPage.drawImage(im_jpg_path, 0, 0, width=page_size['width'], height=page_size['height'], preserveAspectRatio=True)

    def get_scale(self, page_size, image_width, image_height):
        """
        画像のスケールを計算します。ページサイズに合わせて画像を拡大・縮小する際に使用するスケールを返します。

        Parameters
        ----------
        page_size : dict
            ページサイズの情報。keys: 'width', 'height'
        image_width : int or float
            画像の幅。
        image_height : int or float
            画像の高さ。

        Returns
        -------
        scale : float
            画像のスケール。ページサイズに対する画像の縮尺。

        """
        # ページの高さと幅に対する画像のスケールを計算
        p_height_scale = page_size['height'] / image_height
        p_width_scale = page_size['width'] / image_width

        # 画像がページにフィットするように、小さい方のスケールを返す
        return min(p_height_scale, p_width_scale)


    def get_canvases(self):
        """
        IIIFマニフェストからキャンバスのリストを取得します。指定された範囲のキャンバスだけを取得することも可能です。

        Returns
        -------
        canvases : list
            キャンバスのリスト。

        Raises
        ------
        Exception
            サポートされていないコンテキストの場合に発生します。
        """
        
        # マニフェストのURLまたはパスからJSONを取得します
        if self.iiif_manifest_url is not None:
            manifest_json = self.get_manifest_json_from_url(self.iiif_manifest_url)
        elif self.iiif_manifest_path is not None:
            manifest_json = self.get_manifest_json_from_path(self.iiif_manifest_path)
        else:
            raise Exception("Neither IIIF manifest URL nor path provided")

        # コンテキストを検証します
        contexts = manifest_json["@context"]
        if not isinstance(contexts, list):
            contexts = [contexts]
        if contexts[0] != "http://iiif.io/api/presentation/3/context.json":
            raise Exception("Not supported context")

        # キャンバスを取得し、範囲が指定されている場合はその範囲のみを返します
        canvases = manifest_json["items"]
        if self.canvas_range is not None:
            start, end = self.canvas_range
            canvases = canvases[start:end]

        return canvases

    def download_image(self, image_url, i):
        """
        イメージをダウンロードし、そのパスを返します。

        Parameters
        ----------
        image_url : str
            ダウンロードするイメージのURL
        i : int
            イメージのインデックス

        Returns
        -------
        str
            ダウンロードしたイメージのパス
        """
        if self.image_download_dir is None:
            image_hash = hashlib.md5(image_url.encode()).hexdigest()
            img_path = f"{self.task_tmp_dir}/images/{image_hash}.jpg"
        else:
            # print("image_url", image_url)
            filename = image_url.split("/")[-1]
            # img_path = f"{self.image_download_dir}/{str(i+1).zfill(4)}.jpg"
            img_path = f"{self.image_download_dir}/{filename}"

            if not os.path.exists(img_path):
                return None

        if not os.path.exists(img_path):
            os.makedirs(os.path.dirname(img_path), exist_ok=True)
            image_content = requests.get(image_url).content
            with open(img_path, "wb") as img_file:
                img_file.write(image_content)

        return img_path

    def get_img_path(self, iiif_canvas, i):
        """
        イメージのパスを取得します。もし特定のURLが含まれている場合は特定のメソッドを呼び出し、
        そうでなければ画像をダウンロードしてそのパスを返します。

        Parameters
        ----------
        iiif_canvas : dict
            IIIFマニフェストのキャンバス
        i : int
            キャンバスのインデックス

        Returns
        -------
        str
            イメージのパス
        """
        image_url = iiif_canvas["items"][0]["items"][0]["body"]["id"]

        if "https://clioimg.hi.u-tokyo.ac.jp/viewer/api/image/idata" in image_url:
            img_path = self.getHiImagePath(image_url)
        else:
            img_path = self.download_image(image_url, i)

        return img_path

    def convert_iiif2pdf(self, output_path, iiif_manifest_url = None, iiif_manifest_path = None, post_text_size = 0, default_color = "red", default_alpha=0.0, default_main_color = "gray", default_main_alpha = 0.0, canvas_range=None, font_page_limit = 24, task_id = "base", compress_quality = 100, image_download_dir = None, is_copressed = False, dictionary = {}, with_ocr = True):
        """
        IIIFマニフェストをPDFに変換します。

        Parameters
        ----------
        output_path : str
            出力するPDFのパス
        iiif_manifest_url : str, optional
            IIIFマニフェストのURL
        iiif_manifest_path : str, optional
            IIIFマニフェストのファイルパス
        post_text_size : int, optional
            ポストテキストのサイズ
        default_color : str, optional
            デフォルトの色
        default_alpha : float, optional
            デフォルトの透明度
        default_main_color : str, optional
            メインテキストのデフォルトの色
        default_main_alpha : float, optional
            メインテキストのデフォルトの透明度
        canvas_range : list, optional
            キャンバスの範囲 [start, end]
        font_page_limit : int, optional
            フォントサイズの上限
        task_id : str, optional
            タスクID
        compress_quality : int, optional
            圧縮率
        image_download_dir : str, optional
            画像をダウンロードするディレクトリ
        is_copressed : bool, optional
            画像を圧縮するかどうか
        dictionary : dict, optional
            異体字辞書
        with_ocr : bool, optional
            OCRを行うかどうか
        """

        '''
        if isItaiji:
            self.getItaiji()
        '''

        self.task_tmp_dir = f"{self.tmp_dir}/{task_id}"

        self.isItaiji = True if dictionary != {} else False
        self.dictionary = dictionary
        self.font_page_limit = font_page_limit

        self.post_text_size = post_text_size
        self.default_color = default_color
        self.default_alpha = default_alpha
        self.default_main_color = default_main_color
        self.default_main_alpha = default_main_alpha

        self.is_compressed = is_copressed
        self.compress_quality = compress_quality

        self.canvas_range = canvas_range
        self.iiif_manifest_url = iiif_manifest_url
        self.iiif_manifest_path = iiif_manifest_path

        self.image_download_dir = image_download_dir

        if iiif_manifest_url is None and iiif_manifest_path is None:
            raise Exception('iiif_manifest_url or iiif_manifest_path must be specified.')
        
        os.makedirs(os.path.dirname(output_path), exist_ok=True)

        # pass
        newPdfPage = canvas.Canvas(output_path)

        

        canvases = self.get_canvases()

        # for iiif_canvas in tqdm(canvases):
        for i in tqdm(range(len(canvases))):

            iiif_canvas = canvases[i]

            # isVertical = self.estimateVertical(iiif_canvas)

            # isVertical = True

            # isVertical = False

            # pdfmetrics.registerFont(UnicodeCIDFont('HeiseiKakuGo-W5', isVertical=True))
            pdfmetrics.registerFont(UnicodeCIDFont('HeiseiMin-W3', isVertical=True)) # False
            pdfmetrics.registerFont(UnicodeCIDFont('HeiseiKakuGo-W5', isVertical=False)) # False
            

            img_path = self.get_img_path(iiif_canvas, i)

            if img_path is None:
                continue

            # fliped_img_path = img_path + ".fliped.jpg"
            im = Image.open(img_path)
            image_width, image_height = im.size

            # ページサイズ
            page_size = {}

            ratio = image_width / image_height

            # 横長
            if ratio > 1.0 :
                newPdfPage.setPageSize(landscape(A4))
                page_size['width'], page_size['height'] = landscape(A4)
            else: # 縦長
                newPdfPage.setPageSize(A4)
                page_size['width'], page_size['height'] = A4

            # 画像のほうが横に長い、小さい方のスケールを維持する
            scale = self.get_scale(page_size, image_width, image_height)

            self.set_image(newPdfPage, im, img_path, page_size)

            if with_ocr:
                # print(page_size, "image", image_width, image_height, "canvas", iiif_canvas["width"], iiif_canvas["height"])
                self.appendOcr(newPdfPage, scale, page_size, iiif_canvas, image_width, image_height)
                

            newPdfPage.showPage()

        newPdfPage.save()

    def appendText(self, x, y, w, h, text_value, scale4iiif, newPdfPage, postText, image_height, scale, offset_page_x, offset_page_y):
        """
        PDFページにテキストを追加します。

        テキストは指定された座標とサイズ情報に基づいて配置されます。

        Parameters
        ----------
        x : float
            テキストのX座標。
        y : float
            テキストのY座標。
        w : float
            テキストの幅。
        h : float
            テキストの高さ。
        text_value : str
            描画するテキストの値。
        scale4iiif : float
            IIIFのスケール係数。
        newPdfPage : object
            新しいPDFページオブジェクト。
        postText : str
            注釈に追加するテキスト。
        image_height : int
            画像の高さ。
        scale : float
            スケール係数。
        offset_page_x : float
            ページ上のXオフセット。
        offset_page_y : float
            ページ上のYオフセット。
        """

        x1 = x * scale4iiif
        y1 = y * scale4iiif
        w = w * scale4iiif
        h = h * scale4iiif

        

        x2 = x1 + w

        # 代替文字を使用する場合（例：異体字）、辞書を適用します
        if self.isItaiji:
            text_value = "".join(self.dictionary.get(char, char) for char in text_value)

        # テキストが空の場合は処理を中止します
        if not text_value:
            return None
        
        if w > h:
            isVertical = False
        else:
            isVertical = True

        # pdfmetrics.registerFont(UnicodeCIDFont('HeiseiKakuGo-W5', isVertical=isVertical)) # False

        if isVertical:
            # フォントサイズの設定
            text_height = h / len(text_value)
            font_image_size = text_height
            font_page_size = font_image_size * scale
            if font_page_size > self.font_page_limit:
                font_page_size = self.font_page_limit
            newPdfPage.setFont('HeiseiMin-W3', font_page_size)

            # 異なる

            ## x
            x_start = (x1 + x2) / 2
            x_page_start = offset_page_x + scale * x_start

            ## y
            anchor_y = image_height - y1
            start = (anchor_y + text_height * len(""))
            y_page_start = offset_page_y + scale * start
            

            # ページにテキストを描画します
            fixed_text_value = text_value + postText
            newPdfPage.drawString(x_page_start, y_page_start, fixed_text_value)

        else:
            # 横書きのテキストの処理を追加

            # フォントサイズの設定
            text_width = w / len(text_value)
            font_image_size = text_width
            font_page_size = font_image_size * scale
            if font_page_size > self.font_page_limit:
                font_page_size = self.font_page_limit
            newPdfPage.setFont('HeiseiKakuGo-W5', font_page_size)

            # 異なる

            ## x
            x_page_start = offset_page_x + scale * x1

            ## y
            y_page_start = offset_page_y + scale * (image_height - y1 - h)
            

            # ページにテキストを描画します
            fixed_text_value = text_value + postText
            newPdfPage.drawString(x_page_start, y_page_start, fixed_text_value)

        self.add_rectangle_to_page(newPdfPage, scale * x1, scale * y1, scale * w, scale * h, offset_page_x, offset_page_y, line_color="blue", fill_color=None)

    def add_rectangle_to_page(self, newPdfPage, x1, y1, w, h, offset_page_x, offset_page_y, line_color="black", fill_color=None):
        """
        PDFページに矩形を追加します。

        Parameters
        ----------
        newPdfPage : object
            PDFページオブジェクト。
        x1 : float
            矩形の左上のX座標。
        y1 : float
            矩形の左上のY座標。
        w : float
            矩形の幅。
        h : float
            矩形の高さ。
        line_color : str, optional
            矩形の枠線の色。
        fill_color : str or None, optional
            矩形の塗りつぶし色。塗りつぶしを行わない場合はNone。
        """
        newPdfPage.setStrokeColor(line_color)
        newPdfPage.setLineWidth(0.1)
        newPdfPage.setStrokeAlpha(self.default_alpha)

        page_height = newPdfPage._pagesize[1]  # PDFページの高さを取得
        converted_y1 = page_height - (y1 + h) - offset_page_y
        
        if fill_color:
            newPdfPage.setFillColor(fill_color)
            newPdfPage.rect(offset_page_x + x1, converted_y1, w, h, fill=1)
        else:
            newPdfPage.rect(offset_page_x + x1, converted_y1, w, h, fill=0)
    
    def appendGroupAnnotations(self, annotations, scale4iiif, newPdfPage, postText, image_height, scale, offset_page_x, offset_page_y):
        """
        グループ化された注釈をPDFページに追加します。

        注釈は、与えられた座標とテキスト情報に基づいてPDFページに配置されます。

        Parameters
        ----------
        annotations : list
            注釈のリスト。
        scale4iiif : float
            IIIFのスケール係数。
        newPdfPage : object
            新しいPDFページオブジェクト。
        postText : str
            注釈に追加するテキスト。
        image_height : int
            画像の高さ。
        scale : float
            スケール係数。
        offset_page_x : float
            ページ上のXオフセット。
        offset_page_y : float
            ページ上のYオフセット。
        """

        sorted_annotations = self.sort_annotation(annotations)
        prev_group = None

        for i, row in enumerate(sorted_annotations):
            x1, y1, x2 = row["x1"] * scale4iiif, row["y1"] * scale4iiif, row["x2"] * scale4iiif
            h = row["h"] * scale4iiif
            text_value = PdfClient.remove_text_prefix(row["text"])
            postText = self.getPostText(i, sorted_annotations, size=self.post_text_size)

            # 色情報を取得し、PDFページに適用します
            color, alpha = self.get_color(prev_group, row, default_color=self.default_color, default_alpha=self.default_alpha, default_main_color=self.default_main_color, default_main_alpha=self.default_main_alpha)
            newPdfPage.setFillColor(color, alpha=alpha)

            # テキストと座標情報を用いて、PDFページにテキストを追加します
            self.appendText(x1, y1, x2 - x1, h, text_value, scale4iiif, newPdfPage, postText, image_height, scale, offset_page_x, offset_page_y)

            prev_group = row["group"]

    @staticmethod
    def remove_text_prefix(text):
        """
        与えられたテキストから特定の接頭辞（プレフィックス）を削除します。

        テキストが "【" で始まる場合、最初の "】" までの部分が削除されます。

        Parameters
        ----------
        text : str
            接頭辞を削除する対象のテキスト。

        Returns
        -------
        str
            接頭辞が削除されたテキスト。
        """

        if text.startswith("【"):
            prefix = text.split("】")[0] + "】"
            text = text.replace(prefix, "")
        return text

    def appendOcr(self, newPdfPage, scale, page_size, iiif_canvas, image_width, image_height):
        """
        OCR（光学文字認識）を使用して、新しいPDFページにテキストを追加します。

        Parameters
        ----------
        newPdfPage : PDF Page Object
            テキストを追加するPDFページ。
        scale : float
            ページサイズに合わせるための画像のスケール。
        page_size : dict
            ページサイズの情報。keys: 'width', 'height'
        iiif_canvas : dict
            IIIFマニフェストからのキャンバス情報。
        image_width : int
            画像の幅。
        image_height : int
            画像の高さ。
        """

        # 画像のスケールをIIIFキャンバスの高さに合わせます
        # 画像が1024などの場合
        scale4iiif = image_height / iiif_canvas["height"]

        # ページ内のオフセットを計算します
        offset_page_x = (page_size['width'] - image_width * scale) / 2
        offset_page_y = (page_size['height'] - image_height * scale) / 2

        annotations = iiif_canvas["annotations"][0]["items"]

        isSort = False

        if not isSort:
            for i, row in enumerate(annotations):
                target = row["target"]

                if type(target) is dict:
                    # 今後用修正
                    if "target2" in row:
                        target = row["target2"] 
                    else:
                        continue

                xywh = target.split("#xywh=")[1].split(",")

                # group = None

                text_value = PdfClient.remove_text_prefix(row["body"]["value"])

                x1 = int(xywh[0])
                y1 = int(xywh[1])
                w = int(xywh[2])
                h = int(xywh[3])

                postText = ""

                self.setColor(newPdfPage)

                self.appendText(x1, y1, w, h, text_value, scale4iiif, newPdfPage, postText, image_height, scale, offset_page_x, offset_page_y) # group, 

                

            # pass

        else:

            self.appendGroupAnnotations(annotations, scale4iiif, newPdfPage, postText, image_height, scale, offset_page_x, offset_page_y)

    def setColor(self, newPdfPage):
        """
        PDFページに対してデフォルトの色を設定します。

        色のデバッグ情報を出力する場合は、debug変数をTrueに設定します。

        Parameters
        ----------
        newPdfPage : object
            色を設定する対象のPDFページオブジェクト。

        Notes
        -----
        - デフォルトの色はインスタンス変数 `self.default_color` から取得します。
        - 透明度は1.0に固定されています。
        """

        color = self.default_color
        alpha = self.default_alpha

        debug = False
        if debug:
            print(color, alpha)  # 色と透明度のデバッグ情報を出力

        newPdfPage.setFillColor(color, alpha=alpha)  # ページに色を設定

    def get_color(self, prev_group, row, default_color="red", default_alpha=0.5, default_main_color="gray", default_main_alpha=0.5):
        """
        与えられた行と前のグループに基づいて色と透明度を取得します。

        Parameters
        ----------
        prev_group : str
            前の行のグループ。
        row : dict
            処理する行の情報。
        default_color : str, optional
            デフォルトの色。デフォルトは "red"。
        default_alpha : float, optional
            デフォルトの透明度。デフォルトは 0.5。
        default_main_color : str, optional
            本文のデフォルトの色。デフォルトは "gray"。
        default_main_alpha : float, optional
            本文のデフォルトの透明度。デフォルトは 0.5。

        Returns
        -------
        color : str
            選択された色。
        alpha : float
            選択された透明度。
        """

        # 通常の色と透明度を使用します
        color = default_color
        alpha = default_alpha

        # 新しいグループに入った場合、ランダムな色を選択します
        if prev_group != row["group"]:
            color_names = [name for name in dir(colors) if isinstance(getattr(colors, name), colors.Color)]
            color = random.choice(color_names)

        # 本文のタイプの場合、デフォルトの本文の色と透明度を使用します
        if row["type"] == "本文":
            color = default_main_color
            alpha = default_main_alpha

        return color, alpha

    def sort_annotation(self, annotations):
        xy_map = {}

        for annotation in annotations:
            target = annotation["target"]

            if type(target) != str:
                continue

            target_spl = target.split("#xywh=")
            if len(target_spl) == 1:
                continue
            xywh = target_spl[1].split(",")
            x1 = int(xywh[0])
            y1 = int(xywh[1])
            w = int(xywh[2])
            h = int(xywh[3])

            x2 = x1 + w
            y2 = y1 + h

            text = annotation["body"]["value"]

            # text_type
            if text.startswith("【"):
                text_type, text_value = text[1:].split("】")
            else:
                text_type = "本文"
                text_value = text

            row = {
                "x1": x1,
                "y1": y1,
                "x2": x2,
                "y2": y2,
                "text": text_value,
                "type": text_type,
                "w": w,
                "h": h,
            }

            # rows.append(row)

            x_key = str(100000 - x1).zfill(8)
            # y_key = str(y1).zfill(8)

            xy_key = f"{x_key}" # -{y_key}"

            if text_type not in xy_map:
                xy_map[text_type] = {}

            if xy_key not in xy_map[text_type]:
                xy_map[text_type][xy_key] = []

            xy_map[text_type][xy_key].append(row)

        '''
        with open("data/p1/xy_map.json", "w") as f:
            json.dump(xy_map, f, indent=4, ensure_ascii=False)
        '''

        rows2 = []

        for text_type in xy_map:

            rows = []

            tmp_map = xy_map[text_type]

            for xy_key in sorted(tmp_map):
                lines = tmp_map[xy_key]
                for line in lines:
                    rows.append(line)
            

            if text_type == "本文":
                # Initialize the list of groups
                groups = []

                # For each object, check whether it overlaps with an existing group
                for obj in rows:
                    # This flag checks whether the object has been added to a group
                    added = False
                    for group in groups:
                        # If the object overlaps with the group, add it to the group
                        if obj["x1"] <= group[-1]["x2"] and obj["x2"] >= group[0]["x1"]:
                            group.append(obj)
                            group.sort(key=lambda x: x['x1'])  # Sort the group by x_start
                            added = True
                            break
                    # If the object does not overlap with any group, create a new group
                    if not added:
                        groups.append([obj])

                # Print the groups
                for i, group in enumerate(groups):
                    # print(f"Group {i+1}:")
                    # print(json.dumps(group, ensure_ascii=False))

                    tmp = {}

                    for obj in group:

                        y1 = obj["y1"]
                        if y1 not in tmp:
                            tmp[y1] = []

                        tmp[y1].append(obj)

                    for y1 in sorted(tmp):
                        objs = tmp[y1]
                        for obj in objs:
                            obj["group"] = f"{text_type}"
                            rows2.append(obj)

            elif text_type == "頭注":
                objects = rows
                # Calculate the average width of objects
                widths = [(obj["x2"] - obj["x1"]) for obj in objects]
                average_width = sum(widths) / len(widths)

                # Initialize the list of groups
                groups = [[objects[0]]]

                # For each object, check whether it overlaps with the last object in the current group
                for obj in objects[1:]:
                    # If the gap is less than half of the average width, add the object to the current group
                    if groups[-1][-1]["x1"] - obj["x2"]  < average_width / 2:
                        groups[-1].append(obj)
                    else:
                        # Otherwise, start a new group
                        groups.append([obj])

                # Print the groups
                for i, group in enumerate(groups):
                    # print(f"Group {i+1}: {group}")
                    for obj in group:
                        obj["group"] = f"{text_type}-{i + 1}"
                        rows2.append(obj)

            elif text_type == "割注":
                objects = rows
                # Calculate the average width of objects
                widths = [(obj["x2"] - obj["x1"]) for obj in objects]
                average_width = sum(widths) / len(widths)

                # Initialize the list of groups
                groups = [[objects[0]]]

                # For each object, check whether it overlaps with the last object in the current group
                for obj in objects[1:]:
                    # If the gap is less than half of the average width, add the object to the current group
                    if groups[-1][-1]["x1"] - obj["x2"]  < average_width / 2:
                        groups[-1].append(obj)
                    else:
                        # Otherwise, start a new group
                        groups.append([obj])

                # Print the groups
                for i, group in enumerate(groups):
                    # print(f"Group {i+1}: {len(group)} {group}")

                    # このグループのなかで、さらに、y座標に重なりがあるかを調べる

                    groups2 = self.divideByY(group)

                    for j, group2 in enumerate(groups2):

                        for obj in group2:
                            # for obj in group:
                            obj["group"] = f"{text_type}-{i + 1}-{j + 1}"
                            
                            rows2.append(obj)
                            # rows2.append(obj)
            
            else:
                for i, obj in enumerate(rows):
                    obj["group"] = f"{text_type}-{i+1}"
                    rows2.append(obj)

        if self.debug:
            with open(f"{self.tmp_dir}/sorted_annotaions.json", "w") as f:
                json.dump(rows2, f, indent=4, ensure_ascii=False)
        

        return rows2


    def getPostText(self, i, rows, size=5):
        """
        現在の注釈と同じグループを持つ 'rows' リスト内の次の注釈からテキストを取得します。

        Parameters
        ----------
        i : int
            'rows' 内の現在の注釈のインデックス。
        rows : list of dict
            各注釈を辞書として表現したリスト。各辞書は 'group' と 'text' キーを持ちます。
        size : int, optional
            次の注釈テキストから返す文字数（デフォルトは5）。

        Returns
        -------
        str
            現在のものと同じ 'group' を持つ次の注釈からのテキストで、'size' 文字に切り取られたもの。
            そのような注釈が存在しない場合は空の文字列。

        Examples
        --------
        >>> rows = [{"group": "A", "text": "apple"}, {"group": "A", "text": "banana"}, {"group": "B", "text": "cherry"}]
        >>> obj = MyClass()
        >>> obj.getPostText(0, rows)
        'banan'
        """

        # インデックス 'i' の注釈の 'group' 値を取得
        current_type = rows[i]["group"]

        # インデックス 'i+1' から残りの注釈を反復処理
        for j in range(i + 1, len(rows)):
            # 'group' 値が現在の 'group' 値と一致する場合、切り取られたテキストを返す
            if rows[j]["group"] == current_type:
                return rows[j]["text"][:size]

        # 一致する 'group' が見つからない場合、空の文字列を返す
        return ""
    
    def divideByY(self, objects):
        """
        オブジェクトのリストをy座標に基づいてグループ化します。同じグループに属するオブジェクトは、
        y座標で重なり合っている必要があります。

        Parameters
        ----------
        objects : list of dict
            グループ化するオブジェクトのリスト。各オブジェクトは辞書で、'y1' および 'y2' キーを持っています。

        Returns
        -------
        list of list of dict
            オブジェクトのグループのリスト。各グループはオブジェクトのリストです。

        Examples
        --------
        >>> objects = [{"y1": 10, "y2": 20}, {"y1": 15, "y2": 25}, {"y1": 30, "y2": 40}]
        >>> obj = MyClass()
        >>> obj.divideByY(objects)
        [[{"y1": 10, "y2": 20}, {"y1": 15, "y2": 25}], [{"y1": 30, "y2": 40}]]
        """

        groups = []

        # オブジェクトを反復処理して、それぞれを既存のグループに追加するか、新しいグループを作成
        for obj in objects:
            added = False # オブジェクトが既存のグループに追加されたかどうかを示すフラグ
            for group in groups:
                # オブジェクトがグループと重なる場合、オブジェクトをそのグループに追加
                if obj["y1"] <= group[-1]["y2"] and obj["y2"] >= group[0]["y1"]:
                    group.append(obj)
                    group.sort(key=lambda x: -x['x1'])  # グループをxの逆順にソート
                    added = True
                    break

            # オブジェクトが既存のグループに追加されていない場合、新しいグループを作成
            if not added:
                groups.append([obj])

        return groups

    @staticmethod
    def getHiItaiji():
        """
        東京大学の人文情報学環の異体字リストから異体字マップを作成し返します。
        このマップは、異体字（古い文字）を標準字（新しい文字）にマッピングします。

        Returns
        -------
        dict
            異体字と標準字のマッピングを表す辞書。

        Examples
        --------
        >>> itaiji_map = MyClass.getHiItaiji()
        >>> itaiji_map["異体字の例"]
        "標準字の例"
        """

        itaijiMap = {}
        url = "https://wwwap.hi.u-tokyo.ac.jp/ships/itaiji_list.jsp"

        # 指定したURLから異体字リストのページを取得
        r = requests.get(url)
        soup = BeautifulSoup(r.content, "html.parser")

        # 表のすべての行を反復処理
        for tr in soup.find_all("tr"):
            tds = tr.find_all("td")

            # 3つのセルがない場合は、この行をスキップ
            if len(tds) != 3:
                continue

            # 新しい文字と古い文字の取得
            w_new = tds[1].text
            ws_old = tds[2].text.split("\u3000")

            # 異体字マップの作成
            for w_old in ws_old:
                if w_old != "\xa0":
                    itaijiMap[w_old] = w_new

        return itaijiMap

