[PYTHON] Generate a vertical image of a novel from text data

(Addition 2019.12.14)

Pillow can be written vertically in Japanese at least in Ver6 or later (+ raqm 0.7 or later). Please note that the content of this article is out of date.

To do

yosuruni.png

I thought about that.

Constitution

The main libraries used are as follows.

Describe the resolution of the generated image and the layout of columns in xml. There is no particular format on which it is based, and it is an original one that has been decided appropriately. Use the xml library to parse it. The text is written in basic plain text, and ruby and emphasis marks are written in html tags in the text. Use HTMLParser to parse this. I feel that one of the libraries can handle both, but I decided to use both for studying. Use Pillow, an image processing library, to draw characters. Of course, it can't be helped, but Pillow doesn't support vertical writing at all, so I do a lot of trial and error (cutting out) to write vertically.

Layout and body

The xml that determines the layout and the body text look like the following.

yosuruni_layout.xml


<novel width="1920" height="960" margin_up="0.1" margin_bottom="0.1" margin_left="0.05" margin_right="0.05">
    <columnchain name="Main" fontsize="36" direction="VERTICAL" linespace="2.0" color="#101000">
        <column refp="UP_RIGHT" reflh="MARGIN_RIGHT" reflv="MARGIN_UP" offsetx="LIVEAREA_H:0" offsety="LIVEAREA_V:0" sizew="LIVEAREA_H:1.0" sizeh="LIVEAREA_V:1.0"/>
    </columnchain>
    <text columnchain="Main" src="yosuruni.xml" />
</novel>

yosuruni.xml


My child, who was four years old, has become quite readable. I'm thinking of reading a lot of books from now on, but I haven't read much so far, so I didn't know what kind of book to choose.

As a result of various thoughts, I realized that I should write a story myself and have it read. Even though I haven't read a book in the first place, I wonder if I can write a story, but there is no doubt if I write "buttocks" or "shit" appropriately. Because I'm 4 years old.

By the way, when I started writing with a notepad application, it was completely horizontal writing.<d>Not like</d>.. If it is not written vertically, as a "reading material"<r val="Fun">atmosphere</r>I can't feel it, and it's not fun to watch. So I searched for an editor that can write vertically, but I couldn't find anything that looked like this.

If this happens, there is no choice but to make it. An application like an editor is impossible, but I think it can be done with a tool that converts plain text into a vertical image ...

In the layout, if you increase the column elements in the columnchain element, the columns will increase and the text will be flowed in the order of column description.

The layout and body text are basically parsed by the following routines.

Layout analysis.py


import xml.etree.ElementTree as ET

#Parse xml to get element tree
tree = ET.parse(xml_path)
#Get the root element. You get a novel element
novel_element = tree.getroot()

#Here, refer to the attribute of the novel element to get the setting value.

#Get the columnchain element inside the novel element
for cc_element in novel_element.iter("columnchain"):
    #Here, refer to the attribute of the columnchain element to get the setting value.

    #Get the column element inside the columnchain element
    for c_element in cc_element.iter("column"):
        #Here, refer to the attribute of the columnc element to get the set value.

#Get the text element inside the novel element
for text_element in novel_element.iter("text"):
    #Here, refer to the attribute of the text element to get the setting value.

Body analysis.py


from html.parser import HTMLParser

class TextParser(HTMLParser):

    def __init__(self):
        super().__init__()

    def handle_starttag(self, tag, attrs):
        if tag == "ruby" or tag=="r":
            #Detecting the start of ruby tag

        if tag == "dot" or tag=="d":
            #Detects the start of emphasis marks

    def handle_endtag(self, tag):
        if tag == "ruby" or tag == "dot" or tag == "r" or tag == "d":
            #Detect the end of tag

    def handle_data(self, data):
        #Get data in tags. The text itself or ruby(Reading kana sentence)Get

class Text():

    def __init__(self, source):
        parser = TextParser()
        parser.feed(source)

Execution & output result

For example, the following is a vertically long layout with 3 columns. (Since it is an example, the resolution is low. The characters are 12 points at 320x720)

yosuruni_layout.xml


<novel width="320" height="720" margin_up="0.1" margin_bottom="0.1" margin_left="0.05" margin_right="0.05">
    <columnchain name="Main" fontsize="12" direction="VERTICAL" linespace="2.0" color="#101000">
        <column refp="UP_RIGHT" reflh="MARGIN_RIGHT" reflv="MARGIN_UP" offsetx="LIVEAREA_H:0" offsety="LIVEAREA_V:0" sizew="LIVEAREA_H:1.0" sizeh="LIVEAREA_V:0.3"/>
        <column refp="UP_RIGHT" reflh="MARGIN_RIGHT" reflv="MARGIN_UP" offsetx="LIVEAREA_H:0" offsety="LIVEAREA_V:0.35" sizew="LIVEAREA_H:1.0" sizeh="LIVEAREA_V:0.3"/>
        <column refp="UP_RIGHT" reflh="MARGIN_RIGHT" reflv="MARGIN_UP" offsetx="LIVEAREA_H:0" offsety="LIVEAREA_V:0.7" sizew="LIVEAREA_H:1.0" sizeh="LIVEAREA_V:0.3"/>
    </columnchain>
    <text columnchain="Main" src="yosuruni.xml" />
</novel>
$ python NovelFE.py yosuruni_layout.xml

This will output the following image.

test0.png

If the resolution is low, the position of the characters may fluctuate slightly, which is annoying.

Vertical drawing

As mentioned above, Pillow allows horizontal writing, but not vertical writing. So I decided to write vertically. In other words, it draws characters one by one while shifting the position vertically.

I used Gensho Antic as the font. A font for comics that supports vertical writing.

However, when I try to draw it, it is natural,

ng.png

Font glyphs for horizontal writing, such as parentheses, punctuation, and small "tsu", are used. In the Pillow library, there is no way to specify vertical writing, so the glyph for vertical writing is not used anyway.

As a quick and quick idea of what to do, I came up with the idea of tinkering with the font file itself and forcibly replacing the horizontal glyphs with vertical glyphs.

pip install fonttools

Install the Python font tools fonttools (ttx). If you specify a font file with the ttx command, it will be converted to xml.

% ttx GenEiAntiqueN-Medium.otf 
Dumping "GenEiAntiqueN-Medium.otf" to "GenEiAntiqueN-Medium.ttx"...
Dumping 'GlyphOrder' table...
Dumping 'head' table...
Dumping 'hhea' table...
Dumping 'maxp' table...
Dumping 'OS/2' table...
Dumping 'name' table...
Dumping 'cmap' table...
Dumping 'post' table...
Dumping 'CFF ' table...
Dumping 'BASE' table...
Dumping 'GDEF' table...
Dumping 'GPOS' table...
Dumping 'GSUB' table...
Dumping 'VORG' table...
Dumping 'hmtx' table...
Dumping 'vhea' table...
Dumping 'vmtx' table...

The font used this time is in OpenType format, so check the outline of the specifications below.

Introduction to OpenType Specifications (Part 2) [Introduction to OpenType Specifications (Part 2)] (http://qiita.com/496_/items/4f8327fe741cf0c87736) [Introduction to OpenType Specifications (Part 1)] (http://qiita.com/496_/items/f6efb650dcf7e9d2dfe4)

From the above, the important xml generated by ttx are GSUB and cmap.

OpenType roughly contains glyph (font) data with IDs, The cmap table contains a correspondence table showing the glyph data IDs that correspond to the character (eg Unicode) codes. In addition, in the GSUB table, when the glyph used under a specific condition changes, the correspondence table of the change source glyph ID and the change destination glyph ID is shown.

Therefore, the correspondence table showing the glyph ID to be replaced in the case of vertical writing is extracted from the GSUB table, and based on that, the glyph ID of the correspondence table in the cmap table is replaced. Then you should be able to refer to the glyph for vertical writing unconditionally.

Let's write a script for conversion.

otfconv.py


import argparse
import xml.etree.ElementTree as ET

parser = argparse.ArgumentParser()
parser.add_argument("infile")
args = parser.parse_args()

tree = ET.parse(args.infile)
root = tree.getroot()

list_index = []
cid_replace_dic = {}

for gsub_elements in root.iter('GSUB'):
    for featurerecords in gsub_elements.iter('FeatureRecord'):
        for featuretags in featurerecords.iter('FeatureTag'):
            if featuretags.attrib['value'] == "vert" or \
                    featuretags.attrib['value'] == "vrt2" or \
                    featuretags.attrib['value'] == "vtrt":
                for lookuplistindexs in featurerecords.iter('LookupListIndex'):
                    if not lookuplistindexs.get('value') in list_index:
                        list_index.append(lookuplistindexs.get('value'))

    for lookup in gsub_elements.iter('Lookup'):
        if lookup.get('index') in list_index:
            for substitution in lookup.iter('Substitution'):
                cid_replace_dic[substitution.get('in')] = substitution.get('out')


for cmap in root.iter('cmap'):
    for maps in cmap.iter('map'):
        if maps.get('name') in cid_replace_dic.keys():
            maps.set('name', cid_replace_dic[maps.get('name')])

tree.write("output.xml")
$ python otfconv.py GenEiAntiqueN-Medium.ttx

Hopefully an output.xml will be created, which you can ttx back into an OpenType file. By the way, when I did it, I got a conversion error unless I added the following line to the beginning. (That worked, so I haven't looked into too much detail.)

<?xml version="1.0" encoding="UTF-8"?>

Use ttx to switch back from xml to otf.

$ ttx -o TateFont.otf output.xml

Then

test.png

I was able to write like that.

Some required features

I thought that at least ruby and kinsoku processing were necessary to make it into a novel. Also, as an extra point. Ruby is a little troublesome because if the character height of ruby exceeds the corresponding character height of the text, the character spacing of the text must be increased.

test0.png

The other is multi-page support. If the text does not fit on one page, try to generate multiple images with the same layout.

Now you are ready to write a story.

at the end

Recommended Posts

Generate a vertical image of a novel from text data
Generate a MeCab dictionary from Nico Nico Pedia data
Extracted text from image
Generate image text together
Create a data frame from the acquired boat race text data
[Python] Extract text data from XML data of 10GB or more.
I tried using PI Fu to generate a 3D model of a person from one image
Generate a Docker image using Fabric
Make a Santa classifier from a Santa image
Automatically generate collage from image list
[Spark Data Frame] Change a column from horizontal to vertical (Scala)
I made a subtitle file (SRT) from JSON data of AmiVoice
Generate a list of consecutive characters
How to send a visualization image of data created in Python to Typetalk
A story of a person who started aiming for data scientist from a beginner
python + faker Randomly generate a point with a radius of 100m from a certain point
Acquisition of plant growth data Acquisition of data from sensors
Generate a class from a string in Python
Video acquisition / image shooting from a webcam
A memorandum of trouble when formatting data
"Minecraft where a heckler flies" Generate appropriate text with Deep Learning ~ Collect data ~
I tried to automatically generate a port management table from Config of L2SW
A memo to generate a dynamic variable of class from dictionary data (dict) that has only standard type data in Python3
Generate and post dummy image data with Django
Download the image from the text file containing the URL
A memorandum of calling Python from Common Lisp
How to generate a Python object from JSON
Detect General MIDI data from large amounts of MIDI
A story of creating 16 * 16 dots from a Digimon photo
Data cleansing 3 Use of OpenCV and preprocessing of image data
A well-prepared record of data analysis in Python
The transition of baseball as seen from the data
Extract data from a web page with Python
Try to create a battle record table with matplotlib from the data of "Schedule-kun"
Get a list of GA accounts, properties, and views as vertical data using API
Find out the maximum number of characters in multi-line text stored in a data frame