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> </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.