Lazy Text Tool Plugin and japanese vertical text

Okay, I made a map and here is the map: (save it to where krita has permissions to access it)

{
    "version":1,
    "default":{ "charwidth":2, "charmode":2  },
    "single_regex":{
        "[\\u0020]": { "charwidth":2, "name":"Space" },
        "[\\u30FB]":{ "charwidth":2, "fw_height":"[height]/1.33", "name":"middle dot" },
        "[\\u300C\\u300E\\uFF08\\u2014\\uFF5B\\uFF5F\\u3008\\u300A\\u3010\\u3016\\u3018\\u301A]":{ "charwidth":2, "fw_height":"[height]/2", "name":"open parantheses" },
        "[\\u0000-\\u007F]": { "charwidth":1, "name":"Latin" },
        "[\\u3041-\\u3096]": { "charwidth":2, "name":"Hiragana" },
        "[\\u30A0-\\u30FF]": { "charwidth":2, "name":"Katakana" },
        "[\\u3400-\\u4DB5\\u4E00-\\u9FCB\\uF900-\\uFA6A]": { "charwidth":2, "name":"Kanji" },
        "[\\u2E80-\\u2FD5]": { "charwidth":2, "name":"Kanji Radicals" },
        "[\\uFF5F-\\uFF9F]": { "halfwidth":2, "name":"Katakana HalfWidth" },
        "[\\u31F0-\\u31FF\\u3220-\\u3243\\u3280-\\u337F]": { "halfwidth":0, "name":"Symbols" },
        "[\\uFF01-\\uFF5E]":{ "charwidth":2, "name":"FullWidth Alphanumeric" },
        "[\\u3000-\\u303F]":{ "charwidth":2, "name":"punctuation" }
        
    },
    "pair_regex":{
        "[\\u30FB]\\S":{ "charwidth":2, "fw_height":"[height]/1.33", "name":"middle dot" },
        "[\\u3001\\u3002]\\S":{ "charwidth":2, "fw_height":"[height]/2", "name":"end sentence punctuation marks" },
        "[\\u300D\\u300F\\uFF09\\u2015\\uFF5C\\uFF60\\u3009\\u300B\\u3011\u3017\\u3019\\u301B]\\S":{ "charwidth":2, "fw_height":"[height]/2", "name":"close parantheses" }
    }
}

and here is the code: (replace c:/path/to/jpn.json with the map file)

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import re
import time
import json


def openWindow():
    defaultFont = 'Source Han Serif Vertical'
    defaultFontSize = 10.0
    defaultLineWidth = 100
    defaultLineHeight = 100
    defaultAlign = 'center'
    defaultTransform = ''
    defaultCharMode = 1
    fontRules = 'c:/path/to/jpn.json'
    
    rules = None
    
    with open(fontRules) as f:
        rules = json.load(f)
    
    w = QDialog()
    layout = QVBoxLayout(w)
    
    fontCmb = QFontComboBox()
    layout.addWidget(fontCmb)
   
    hlayout = QHBoxLayout()
    
    fontSize = QDoubleSpinBox()
    hlayout.addWidget(QLabel("Font Size:"))
    hlayout.addWidget(fontSize)

    widthSize = QDoubleSpinBox()
    widthSize.setMaximum(200)

    hlayout.addWidget(QLabel("Line Width:"))
    hlayout.addWidget(widthSize) 

    heightSize = QDoubleSpinBox()
    heightSize.setMaximum(200)

    hlayout.addWidget(QLabel("Line Height:"))
    hlayout.addWidget(heightSize)
    
    layout.addLayout(hlayout)

    textBox = QPlainTextEdit()
    layout.addWidget(textBox)

    hlayout2 = QHBoxLayout()

    button1 = QPushButton("Add Text")
    button1.clicked.connect(w.accept)
    hlayout2.addWidget(button1)

    editContent = readSvgContent()
    print ("cont", editContent)
    if editContent is not None:

        match = re.compile('^.*?\<text.*?id="(vf_.*?)".*$', re.DOTALL).search(editContent)
        
        if match:
            matchData = match.group(1).split('_')
            defaultLineWidth = float(matchData[1])
            defaultLineHeight = float(matchData[2])
            matchTransform = re.compile('^.*?\<text.*?transform="(.*?)".*$', re.DOTALL).search(editContent)
            if matchTransform: defaultTransform = matchTransform.group(1)
            matchFont = re.compile('^.*?\<text.*?font-family="(.*?)".*$', re.DOTALL).search(editContent)
            if matchFont: defaultFont = matchFont.group(1)    
            matchFontSize = re.compile('^.*?\<text.*?font-size="(\d+)".*$', re.DOTALL).search(editContent)
            if matchFontSize: defaultFontSize = float(matchFontSize.group(1))
    
            editContent = re.sub('\<tspan[^\>]+?y="0">\s*<\/tspan>','<p>&nbsp;</p>',editContent)
            editContent = editContent.replace('<tspan>','\n<p>')
            
            print ("sethtml",editContent)
            textBox.document().setHtml(editContent)
            print ( textBox.document().toHtml() )
            button1.setText("Edit Text")
        else:
            editContent = None




    button2 = QPushButton("Cancel")
    button2.clicked.connect(w.reject)
    hlayout2.addWidget(button2)
    
    layout.addLayout(hlayout2) 
    
    def changeFont():
        font = textBox.document().defaultFont()
        font.setFamily(fontCmb.currentFont().family())
        textBox.document().setDefaultFont(font)
        textBox.setPlainText(textBox.toPlainText())

    fontCmb.currentFontChanged.connect(changeFont)

    def changeFontSize():
        font = textBox.document().defaultFont()
        font.setPointSizeF(fontSize.value())
        textBox.document().setDefaultFont(font)
        textBox.setPlainText(textBox.toPlainText())

    #fontSize.valueChanged.connect(changeFontSize)

    fontCmb.setCurrentFont(QFont(defaultFont))
    changeFont()
    fontSize.setValue(defaultFontSize)
    #changeFontSize()
    widthSize.setValue(defaultLineWidth)
    heightSize.setValue(defaultLineHeight)

    w.show()
    if w.exec_() == 0: return
    
    def align(fw,gw,loc=defaultAlign):
        if loc == 'center':
            return (fw-gw/2)
        elif loc == 'right':
            return (fw-gw/2)
        elif loc == 'left':
            return fw    
            
   
    lines = textBox.toPlainText().split("\n")
    fontWidth = 0
    hPos = 0
    pretty = "\n"

    blockCount = textBox.document().blockCount()
    iblock = textBox.document().begin()
    
    outFontSize = fontSize.value() 
    
    font = QFont(fontCmb.currentFont().family(), outFontSize)
    metrics = QFontMetricsF(font)
    hcutWidth = metrics.boundingRect('W')
    
    #p = QPainter()
    #device = QOpenGLPaintDevice()
    #p.begin(device)
    #p.setFont(QFont(fontCmb.currentFont().family(), (outFontSize/96)*Krita.instance().activeDocument().resolution()  ))
    #pMetrics = p.fontMetrics()


    #fontWidth = metrics.averageCharWidth()
    fontWidth = outFontSize * 1.008
    hPos = (blockCount * fontWidth * widthSize.value()) / 100
    
    hX = heightSize.value()
    wX = widthSize.value()
    
    fontName=font.family()
    altFontName=None
    altFont = None
    altMetrics = None
    if fontName.find(' Vertical'):
        altFontName=fontName.replace(' Vertical',' Rotated')
        altFont = QFont(altFontName, outFontSize)
        altMetrics = QFontMetricsF(font)

    print ( "Font Metrics", font.family(), fontWidth, metrics.maxWidth(), metrics.horizontalAdvance('W'), metrics.lineWidth(), metrics.leftBearing('x'), metrics.minLeftBearing(), metrics.minRightBearing(), "ascent", metrics.ascent(), metrics.capHeight(), metrics.height(), "xheight", metrics.xHeight(), metrics.leading(), metrics.lineSpacing(), metrics.descent() )
    print ( metrics.tightBoundingRect('.'), metrics.tightBoundingRect('a'), metrics.averageCharWidth()  )
    #return
    
    def checkRules(char):
        mrl = rules['default'].copy()
        for key in rules['single_regex']:
            if re.search(key, str(char)):
                mrl = { **mrl ,**rules['single_regex'][key] }
                break
        return mrl
        
    def checkPairRules(chars,mrl):
        for key in rules['pair_regex']:
            if re.search(key, str(chars)):
                mrl = { **mrl ,**rules['pair_regex'][key] }
                return mrl
        return mrl
         
    
    def solver(expr):
        if re.search('[^0-9\(\)\.\+\-\/ \*]', expr):
            print ("Invalid equation!")
            return 0
        return eval(expr)    
    
    output = '<text '
    output += 'id="vf_'+ str(widthSize.value()) +'_'+ str(heightSize.value()) +'_'+str(time.time())+'" '
    output += 'transform = "'+defaultTransform+'" '
    output += 'font-family="'+fontName+'" '
    output += 'font-size="'+str(outFontSize)+'">' + pretty    
    while iblock != textBox.document().end():
        blockText = list(iblock.text())
        blockLineCount = iblock.layout().lineCount()
        fontHeight = outFontSize * 1.008
        output += '<tspan y="0">' + pretty
        for i in range(blockLineCount):
            line = iblock.layout().lineAt(i)
            
            chars = []
            hwCount = [0]
            hwGroup = 1

            for i2 in range(line.textLength()):
                glyph = line.glyphRuns(line.textStart()+i2, 1)[0]
                r = glyph.boundingRect()
                r = QRectF( 0,0, r.width()*(outFontSize/10), r.height()*(outFontSize/10) )
                
                rl = checkRules(blockText[i2])
                print ("RL", blockText[i2], hex(ord(blockText[i2])),  rl)
                #print ("G", glyph.rawFont().maxCharWidth(), glyph.rawFont().averageCharWidth(), glyph.boundingRect().width(), glyph.boundingRect().height(), blockText[i2], glyph.glyphIndexes(), glyph.rawFont().glyphIndexesForString(blockText[i2]) )
                #return
                #print ( glyph.rawFont().descent(), blockText[i2], r, metrics.tightBoundingRect(blockText[i2]), metrics.averageCharWidth() )
                
                halfWidth = None
                onCount = 0
                print ( blockText[i2],  glyph.rawFont().pixelSize(), r.width() ,'>', metrics.averageCharWidth()/1.38 )
                #print ("x", blockText[i2], pMetrics.boundingRect(blockText[i2]).width(), '>', pMetrics.averageCharWidth()/1.25)
                #if pMetrics.boundingRect(blockText[i2]).width() > pMetrics.averageCharWidth()/1.25 or blockText[i2] == ' ':
                if rl['charwidth'] == 2:
                    halfWidth = 0
                    if i2 > 0 and chars[i2-1]['hw'] > 0:
                        hwGroup+=1
                else:
                    halfWidth = hwGroup
                    if hwGroup == len(hwCount)-1:
                        hwCount[hwGroup]+=1
                    else:
                        hwCount.append(1)
                    print ( blockText[i2], halfWidth, hwCount[hwGroup] )
                    onCount=hwCount[hwGroup]

                #fontHeight = r.height()
                
                chars.append({ 'glyph': glyph, 'rect': r, 'char':blockText[i2], 'hw': halfWidth, 'hwCount': onCount, 'height': fontHeight, 'skip':False, 'rule': rl  })
                
                
                

            for i2 in range(line.textLength()):
                c = chars[i2]
                
                if c['skip'] is True:
                    continue

                print ('hw', c['char'], c['hw'], hwCount[c['hw']] )
                if defaultCharMode == 0 or c['hw'] == 0 or hwCount[c['hw']] <= 1:
                    print("HO", c['char'], (metrics.ascent()* hX)/100, altMetrics.tightBoundingRect(c['char']), c['rect'] )
                    oHeight = 0 if i2 == 0 else ((chars[i2-1]['height']*hX)/100)
                    oWidth = ( align(fontWidth,c['rect'].width()) * wX)/100

                    if i2 > 0: c['rule']=checkPairRules(chars[i2-1]['char']+c['char'], c['rule']) 
                    
                    if 'fw_height' in c['rule']:
                        print ("SOLVE",c['char'], oHeight, c['rule']['fw_height'].replace('[height]',str(oHeight)))
                        oHeight=solver(c['rule']['fw_height'].replace('[height]',str(oHeight)))
                    
   
                    '''
                    if c['char'] == '「' :
                        oHeight=oHeight / 2
                    elif c['char'] == '(' :
                        oHeight=oHeight / 2
                    elif c['char'] == '・' :
                        oHeight=oHeight / 1.33
                    elif i2 >0 and chars[i2-1]['char'] == '」':
                        oHeight=oHeight / 2
                    elif i2 >0 and chars[i2-1]['char'] == ')':
                        oHeight=oHeight / 2
                    elif i2 >0 and chars[i2-1]['char'] == '、':
                        oHeight=oHeight / 2
                    elif i2 >0 and chars[i2-1]['char'] == '・':
                        oHeight=oHeight / 1.33
                    '''
                        
                    #if i2 > 0 and altMetrics.tightBoundingRect(chars[i2-1]['char']).height() < (metrics.ascent()* hX)/100:
                    #    oHeight = (( altMetrics.tightBoundingRect(chars[i2-1]['char']).height()) )/100              
                    
                    output += ('<tspan '
                    #'font-family="'+fontName+'" '
                    'dy="'+str( oHeight )+'" '
                    'x="'+str(hPos + oWidth  )+'">'
                    +c['char']+
                    '</tspan>' + pretty)
                elif hwCount[c['hw']] == 2:
                    oHeight = 0 if i2 == 0 else ((chars[i2-1]['height']*hX)/100)
                    oWidth = ( align(fontWidth, c['rect'].width()+chars[i2+1]['rect'].width() ) * wX)/100
                    output += ('<tspan '
                    #'font-family="'+fontName+'" '
                    #'dy="'+str( (fontHeight*heightSize.value())/100 )+'" '
                    'dy="'+str( oHeight )+'" '
                    'x="'+str(hPos + oWidth  )+'">'
                    +c['char']+chars[i2+1]['char']+
                    '</tspan>' + pretty)
                    chars[i2+1]['skip']=True
                else:
                    oWidth=(align( c['rect'].height()/2,c['height'])*wX)/100
                    oHeight = 0 if i2 == 0 else (( chars[i2-1]['rect'].width()/2 * hX)/100)
                    if c['hwCount']==1:
                        oHeight=oHeight / 1.07
                    else:
                        oHeight=oHeight * 1.6
                    oWidth-=altMetrics.tightBoundingRect(c['char']).x()
                    c['height']= c['height']
                    print ("M", c['char'], metrics.size(0,'123'), altMetrics.tightBoundingRect(c['char']), 'height', oHeight, metrics.horizontalAdvance(c['char']), metrics.leftBearing(c['char']), metrics.rightBearing(c['char']) )

                    #oWidth-=((metrics.ascent()* hX)/100)-altMetrics.tightBoundingRect(c['char']).width()
                    #oWidth+=((metrics.ascent()* hX)/100)-c['rect'].width()   
                    #oWidth-=((metrics.ascent()* hX)/100)-altMetrics.tightBoundingRect(c['char']).width()
                    #print("HO", c['char'], (metrics.ascent()* hX)/100, altMetrics.tightBoundingRect(c['char']), c['rect'] )    
                    output += ('<tspan '
                    'dy="'+str( oHeight )+'" '
                    'x="'+str(hPos + oWidth)+'" '
                    #'style="font-family: \''+altFontName+'\'">'
                    'font-family="'+altFontName+'">'
                    #'font-family="Roboto Thin">'
                    +c['char']+
                    '</tspan>' + pretty)

                
                #if metrics.tightBoundingRect(blockText[i2]).height() < 40:
                #    fontHeight = r.height() /2 
                #fontHeight = metrics.tightBoundingRect(blockText[i2]).height() - rawFont.descent()
       
        iblock = iblock.next()
        hPos -= (fontWidth * widthSize.value()) / 100
        output += '</tspan>' + pretty
    output += '</text>' + pretty    

    
    
    doc = Krita.instance().activeDocument()    

    svgWidth = str( (doc.width()/72)*doc.resolution() )
    svgHeight = str( (doc.height()/72)*doc.resolution() )
 

    svgContent = '''<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<!-- Created using Krita: https://krita.org -->
<svg xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:krita="http://krita.org/namespaces/svg/krita"
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    width="''' + svgWidth + '''pt"
    height="''' + svgHeight + '''pt"
    viewBox="0 0 ''' + svgWidth + ' ' + svgHeight + '''">
<defs/>''' + output + "</svg>"    
    print( svgContent )
    writeSvgContent(svgContent, doc.activeNode(), (editContent is not None) )
    

def writeSvgContent(svgContent, layer, editMode):
                mimeOldContent=QGuiApplication.clipboard().mimeData();
                mimeStoreContent=QMimeData() 
                for mimeType in mimeOldContent.formats(): 
                    mimeStoreContent.setData(mimeType,QByteArray(mimeOldContent.data(mimeType))) 
                if editMode: Krita.instance().action('edit_cut').trigger()
                mimeNewContent=QMimeData()
                mimeNewContent.setData('image/svg', svgContent.encode())
                QGuiApplication.clipboard().setMimeData(mimeNewContent)
                Krita.instance().action('edit_paste').trigger()
                QGuiApplication.clipboard().setMimeData(mimeStoreContent)
                return None

   

def readSvgContent():
    returnContent = None
    node = Krita.instance().activeDocument().activeNode()
    if node.type() != 'vectorlayer': return None
    mimeOldContent=QGuiApplication.clipboard().mimeData();
    mimeStoreContent=QMimeData() 
    for mimeType in mimeOldContent.formats(): 
        mimeStoreContent.setData(mimeType,QByteArray(mimeOldContent.data(mimeType))) 
                    
    Krita.instance().action('edit_copy').trigger()
    mimeContent=QGuiApplication.clipboard().mimeData();

    for mimeType in mimeContent.formats(): 
        if mimeType.startswith('image/svg'):
            returnContent = str( QByteArray(mimeContent.data(mimeType)) , 'utf-8')
            break
    
    QGuiApplication.clipboard().setMimeData(mimeStoreContent)    
    return returnContent


openWindow()

So from my understanding, how novels handle things and how manga handle things is different. So probably the novel version is the correct one, and the CSP one is a condensed one. Does novel also put 2 characters together like that?

Overall, supporting both proper and manga version wouldn’t be a problem, just have the ability to swap map files.

1 Like