c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

正字で組版する技術

遅れながらであるがはてなエンジニア Advent Calendar 2020 13 日目である。

近頃趣味を遂行するのに組版をする機會が度々有りその事を書く。例へば以下を發刊する爲等だ。

c4se.booth.pm

具体的な組版の手順は Adobe InDesign に流し込んで注意深く校訂するだけだから省く。自分向けの手順が以下に記してある。

scrapbox.io

さてこの組版では、できるだけ正字で組む事を拘はってゐる。その爲に行使する電算技術と、その技術の今後の展開がこの記事の内容となる。

正字とは

そもそも正字とは何かと云ふ說明をしなければならない。正字は一般に舊字・舊字體と呼ばれてゐるものに似るが、概念としては全く異なるものである。舊字とは昭和初期迄日本國で公式に用ゐられてゐた漢字字體である。今我々が用ゐてゐる漢字は、内閣から告示された常用漢字 (新字体) を代表とし、常用漢字に無い舊字體から受け継いだ字、常用漢字に有る字の舊字體、それに多種類に亙る非公式の字を合はせた集合である。この記事で使ってゐる字が概ね舊字體に近い。

正字の反對語は俗字である。誤字は單に使ふべきでない字を使ってゐる事を指すが、文脈に依っては俗字を含む廣い概念である。

或る字には幾つかの下位範疇が有る。先づ字種、これ等は同一の字であると云ふ集合が在る。次に字體、或る字種をどう云ふ筆劃で書くかを決めるもので、異體字 (e.g. 体は體の異體字) はこの範疇の區別である。次に字形、字體の筆劃を、拂ひや跳ね、長さや繋がり等筆法を決めるものである (e.g. 空󠄁と空。font に依っては區別が附かない)。次に書體、篆書・隷書・草書・行書・楷書の五體を代表とし、明朝體やゴシック體等或る字體をどう云ふ見た目で書くかを決めるものだ。最後に具體的な書影が在る。この順序では特に書體の位置附けは曖昧で、字種の次に置いてもよささうである。また字の地域差等、字種も嚴密に定めるのは文脈に依っては困難な事がある。正字は、或る字種の中で具體的な書影より上の抽象的な字體・字形・書體の中でどれが正であり殘りが俗であるかを決めるものだ。五體の中では隋以降は楷書が正字とされる。隋頃に正字として決められ以後通用した書體を楷書と呼ぶのである。

或る字種の中でどれが正字かを決めるやり方には三通り考へられる。

  1. 字源に依るもの。甲骨文や金石文を含む古い資料から字源を特定し、字源と楷書の對應から字形を決める。對應は字の歴史的變化に依存する。字源を調べるのが先づ難しい。字源と楷書の對應も自明ではない。對應規則適用の漏れや過剰適用、また正統な對應と俗な對應との區別を如何にするかが問題となる。
  2. 權威有る書籍に依るもの。康煕字典や說文解字に依る事が多い。何を權威と見做すか何を權威でないと見做すかに問題が有り、また權威同士で矛盾する事もある。
  3. 權威有る傳統に依るもの。權威有る書籍に漏れてゐる、またはそれに反しても傳統的に通用した字體や字形を正と見做す事がある。これも何を權威と見做すか、また權威同士の矛盾が問題となる。

三つの決め方のそれぞれに問題が有る事に加え、三つが互ひに矛盾する事もある。

例を舉げよう。

今「月」と書く部位は三つの字源が混同されてゐる。一つは天體の月、肉を表すにくづき、それに舟を表すふなづきである。この三つは字源も形も異なるが、よく似る爲混同もされてきた。「有」と云ふ字の月は說文解字では天體の月に由來すると考へられ、康煕字典もこれを踏襲してゐた。字體は有󠄁 󠄁である。しかし近年甲骨文資料等から、これはにくづきである事が判った。これに從ふならば字體は有󠄀である。どちらをとるか迷う事になる。

他にもと云ふ字は康煕字典にはこの形で載ってゐる。しかし說文解字にはの形で載ってゐる。これは說文解字より後に變化した字であり、どちらの權威をとるか迷う。

𠏹は地名の𠏹沢に使はれる字だ。これは佛の異體字であるから、基本的には佛に直されるべきものだ。しかし地名の𠏹沢を佛沢とするべきだらうか。この場合は固有名詞を例外とする事でよいかもしれないが、全ての固有名詞を例外とするべきであらうか。また𠏹の字が使はれてゐた文脈に於いて論じてゐる場合、佛に直せば誤りと成るかもしれない。

正字は、かう云ったややこしい例を研究しながら正字とは何かを考へ直して決めるものである。何かの database を參照すればよいものではない訣だ。

正字の概念は可能かと云ふ問題も有る。歴史的に正字概念が成立してきた事から見て、細部で大きく破綻しつつも大域的には成り立つのではないかと楽觀してゐる。

正字に關はる Unicode の知識

正字 - 俗字を電算機で處理する時普通は Unicode で扱ふ事になる。その時知っておくべき知識が在る。

先づは IVS (Ideographic Variation Sequence) の存在。漢字の codepoint に IVS と呼ぶ codepoint を續ける事で字形を變へられる。二つの codepoint で一つの字を表す仕組みは、繪文字を處理し慣れてゐる皆さんには親しい仕組みだらう。正字は基本的に IVS 附きで表す事になる。

次に CJK 互換漢字と SVS (Standardized Variants Sequence) の存在。今は CJK 互換漢字無しで漢字の處理をできる樣になってゐ、混亂の元だから避けるべきである。

最後に國別 font の事。例へば日本・中國・臺灣で使ふ字形は大きく異る。Unicode ではこれを區別せず、font 側で實裝する事になってゐる。同じ Adobe Source Han font を使ってゐても、國毎に異なる font file が用意されてゐる。

正字を一覧する data 構造

先づ字種の中でどれが正字で殘りが俗字かを一覧する必要がある。JSON schema で書くと以下の data 構造が在ればよいだらう。

segzi.yaml

type: array
items:
    type: object
    required: [segzi, konkyo, zokuzi]
    properties:
        segzi:
            description: |
                IVS が附けられるものには附ける。SVS は使はない
                IVS と 5 桁以上の Unicode の字を直接書くのは避け、U~と表記する
            type: string
            pattern: ".|(U\\+[0-9A-F]+(U\\+E01[0-9A-F]{2})?)"
        konkyo: {type: string}
        zokuzi:
            type: array
            items: {type: string, pattern: ".|(U\\+[0-9A-F]+(U\\+E01[0-9A-F]{2})?)"}
        CJK_unified_ideographs: {type: string, pattern: ".|(U\\+([0-9A-F]{4}))"}
        JIS_4th: {type: string, pattern: ".|(U\\+[0-9A-F]+)"}
        JIS_2nd: {type: string, pattern: ".|(U\\+[0-9A-F]+)"}

逆に一つの俗字に複數の正字が合流した場合も一覧しておくとよい。例へば弁には弁・辨・辯・瓣・辮が合流してゐる。

kakinahosi.yaml

type: array
items:
    type: object
    required: [zokuzi, segzis]
    properties:
        zokuzi:
            type: string
        segzis:
            type: array
            items:
                type: object
                required: [segzi, jogxafu]
                properties:
                    segzi: {type: string}
                    jogxafu: {type: string}

變換 program

Python で書くとこうなる。

"""Force 俗字 to 正字."""
import re
import typing as t


class Segzify(object):
    """Force 俗字 to 正字."""

    itaizi_selectors: t.List[str] = [
        "\U000E0100",
        "\U000E0101",
        "\U000E0102",
        "\U000E0103",
        "\U000E0104",
   ]

    table: t.Dict[str, str] = {
        "\u4E07": "\u842C",  # 万
        "\u4E08": "\u4E08\U000E0101",  # 丈
        # ここに俗字 -> 正字を一覧する
    }

    def force(self, content: str) -> str:
        """Force 俗字 to 正字."""
        for (zokuzi, segzi) in self.__class__.table.items():
            regex = "{}(?:[{}]?)".format(
                zokuzi,
                "".join(self.__class__.itaizi_selectors),
            )
            content = re.sub(regex, segzi, content)
        return content

    # __pragma__("skip")
    def force_file(self, filename: str) -> bool:
        """Force 俗字 to 正字 in the file. Return True if the file had some 俗字."""
        with open(filename, "r+") as f:
            original_content = content = f.read()
            content = self.force(content)
            if original_content == content:
                return False
            f.seek(0)
            f.truncate()
            f.write(content)
        return True

    # __pragma__("noskip")

Segzify().force("俗字") と呼ぶ。或る codepoint を IVS で區別したものは同じ字體であって、その中に正字は一つだけと前提してゐる。

識別子のローマ字は Segsyoxafu である。

segsyoxafu.wordpress.com

與太話として上記 code で Markdown を變換する作業をしてゐる時に Prettier が IVS を認識できない事が判った。以下の pull request で直してある。

github.com

Adobe InDesign 上での變換

ここ迄が既に實現してゐる事だ。今組版作業は、Markdown 上で俗字を直し、Adobe InDesign に地道に流し込んでゐる。流し込みが大變手間なので、正字の間違ひが見附かった時に直すのが難しい。InDesign 上で正字を直せればこの問題はなくなる。

InDesignJavaScript で擴張を書ける。これを利用する。

Python から JavaScript への transpile には Transcrypt を使ふ。因みに今の Transcrypt は壞れてゐ、Python 3.7.x で使はなければならない。

github.com

asdf local python 3.7.9
poetry init
poetry add -D Transcrypt
npm init
npm install --save-dev transcrypt-loader webpack webpack-cli

撰擇した範圍を正字に變換する擴張はこうなる。

import typing as t

from segzify import Segzify

alert: t.Any = 0  # __:skip


def do_wrok(text):
    text.contents = Segzify().force(text.contents)


def main():
    if len(app.documents) == 0:
        alert("Error: No document.")
        return
    if len(app.selection) == 0:
        alert("Error: No selection.")
        return
    if len(app.selection) != 1:
        alert("Error: Too many {0} selection.".format(len(app.selection)))
        return
    selection = app.selection[0]
    constructor = selection.constructor.name
    if constructor in [
        "Character",
        "InsertionPoint",
        "Line",
        "Paragraph",
        "Story",
        "Text",
        "TextColumn",
        "TextStyleRange",
        "Word",
   ]:
        do_wrok(selection)
    elif constructor == "TextFrame":
        do_wrok(selection.texts.item(0))
    else:
        alert("Error: The selected {0} is not a text.".format(constructor))


main()

太字や縱中橫等をうまく扱へないのが今の困りどころである。

これを JavaScript に transpile しよう。Webpack を設定する。

"use static";

module.exports = {
  entry: {
    InDesign: "./InDesign.py",
  },
  output: {
    filename: "dist/[name].jsx",
    path: __dirname,
  },
  module: {
    rules: [
      {
        test: /\.py$/,
        loader: "transcrypt-loader",
        options: {},
      },
    ],
  },
  target: "node",
};

poetry run npx webpack で transpile する。これを InDesign の擴張 directory に置けば使へる。