btex: img tag with src attrib

This commit is contained in:
minjaesong
2024-05-16 17:07:36 +09:00
parent e308e9a356
commit efbdc806ea
6 changed files with 142 additions and 29 deletions

View File

@@ -162,18 +162,19 @@
<section>Figures</section> <section>Figures</section>
<p>Figures, or external images can be inserted using the self-closing <code>img</code> tag. <p><index id="img (tag)"/>Figures, or external images can be inserted using the self-closing <code>img</code> tag.
This tag inserts the image starting from the current line; if the size is taller than the This tag inserts the image at the centre of the page, starting from the current line;
remaining lines, the image will be printed onto the next page. Its syntax is as follows:</p> if the size is taller than the remaining lines, the image will be printed onto the next page.
Its syntax is as follows:</p>
<callout align="left" class="code"><!-- <callout align="left" class="code"><!--
-->&lt;img src="web URL" height="8"/&gt;<br/><!-- -->&lt;img src="http(s) or file URL" height="8"/&gt;<br/><!--
-->&lt;img fromgame="basegame:gui/small.png" height="4"/&gt;<br/><!-- -->&lt;img fromgame="basegame:gui/small.png" height="4"/&gt;<br/><!--
-->&lt;img gameitem="basegame:33" height="1"/&gt; -->&lt;img gameitem="basegame:33" height="1"/&gt;
</callout> </callout>
<p>The <code>height</code> attribute specifies the height of the image <emph>in the number of lines</emph>, <p>The <code>height</code> attribute specifies the height of the image <emph>in the number of lines</emph>,
rather than pixels.</p> rather than pixels, the width is calculated automatically. Image width wider than the text width will cause an error.</p>
<p>Supported image formats: JPEG, PNG, BMP or TGA</p> <p>Supported image formats: JPEG, PNG, BMP or TGA</p>
@@ -295,6 +296,22 @@
<part>Non-laymen Zone</part>
<chapter alt="The Waiting Process">Why Wait for the Books to be Printed? Why Cant I just Print Them by Myself?</chapter>
<p>The <btex/> engine is not fast; it takes at least a few seconds to print a book. The “waiting”
system is there because the book is being printed in the background on separate threads
(yes, they are multi-threaded!) to not interfere with the normal gameplay, or else the players will
encounter the freezing every time the book is being printed, and this would be a huge minus towards
the gameplay experience. If the process exits without any error, the mailing system will be
notified and will send the mail containing finished books to the player; if the process exits
with errors, the mail containing details of the errors will be sent instead.</p>
<p>For this reason the “printing press” is not exposed to the player, they only get to interact with it
indirectly through the “publisher” via mail.</p>
<newpage/> <newpage/>

View File

@@ -77,10 +77,10 @@
<p><index id="표지 정의문"/><code>cover</code> 태그는 책의 표지를 지정한다. 표지가 없는 책이라면 이 부분은 생략 가능하다. 표지 정의문의 구조는 다음과 같다.</p> <p><index id="표지 정의문"/><code>cover</code> 태그는 책의 표지를 지정한다. 표지가 없는 책이라면 이 부분은 생략 가능하다. 표지 정의문의 구조는 다음과 같다.</p>
<callout align="left" class="code"><index id="cover (태그)"/><index id="title (태그)"/><index id="subtitle (태그)"/><index id="author (태그)"/><index id="edition (태그)"/>&lt;cover hue="358"&gt;<br/> <callout align="left" class="code"><index id="cover (태그)"/><index id="title (태그)"/><index id="subtitle (태그)"/><index id="author (태그)"/><index id="edition (태그)"/>&lt;cover hue="358"&gt;<br/>
  &lt;title&gt;책 제목&zwsp;&lt;/title&gt;<br/> &lt;title&gt;책 제목&zwsp;&lt;/title&gt;<br/>
  &lt;subtitle&gt;필요한 경우 부제목&zwsp;&lt;/subtitle&gt;<br/> &lt;subtitle&gt;필요한 경우 부제목&zwsp;&lt;/subtitle&gt;<br/>
  &lt;author&gt;누가 이 책을 집필하였는가&zwsp;&lt;/author&gt;<br/> &lt;author&gt;누가 이 책을 집필하였는가&zwsp;&lt;/author&gt;<br/>
  &lt;edition&gt;필요한 경우 판본 정보&zwsp;&lt;/edition&gt;<br/> &lt;edition&gt;필요한 경우 판본 정보&zwsp;&lt;/edition&gt;<br/>
&lt;/cover&gt; &lt;/cover&gt;
</callout> </callout>
<p>이 중 <code>title</code> 태그만이 필수 태그이다. 표지의 문구는 글자간 간격이 넓게 인쇄된다. 책 제목은 두배 큰 크기로 인쇄된다.</p> <p>이 중 <code>title</code> 태그만이 필수 태그이다. 표지의 문구는 글자간 간격이 넓게 인쇄된다. 책 제목은 두배 큰 크기로 인쇄된다.</p>
@@ -156,6 +156,21 @@
</ul> </ul>
<section>그림</section>
<p><index id="img (태그)"/>외부 이미지는 스스로 닫는 <code>img</code> 태그를 사용해 넣을 수 있다. 그림은 세로로는 현재 줄에서부터, 가로로는 페이지의 중앙에 인쇄된다. 페이지에 남아 있는 줄보다 이미지의 높이가 더 크다면, 이미지는 새로운 페이지에 인쇄된다. <code>img</code> 태그의 구조는 다음과 같다.</p>
<callout align="left" class="code"><!--
-->&lt;img src="http(s)나 file URL" height="8"/&gt;<br/><!--
-->&lt;img fromgame="basegame:gui/small.png" height="4"/&gt;<br/><!--
-->&lt;img gameitem="basegame:33" height="1"/&gt;
</callout>
<p><code>height</code> 속성은 이미지의 높이를 <emph>픽셀 단위가 아닌 줄의 개수</emph>로 지정하어야 한다. 이미지의 너비는 자동으로 계산된다. 텍스트의 너비보다 넓다면 오류가 발생한다.</p>
<p>지원하는 이미지 포맷: JPEG·PNG·BMP·TGA</p>
<chapter>색인 페이지</chapter> <chapter>색인 페이지</chapter>
<p><index id="색인 페이지"/>색인 페이지의 내용은 원고를 읽어 자동으로 채워진다. <p><index id="색인 페이지"/>색인 페이지의 내용은 원고를 읽어 자동으로 채워진다.

View File

@@ -8,9 +8,8 @@
<!ENTITY % OLStyle "CDATA"> <!ENTITY % OLStyle "CDATA">
<!ENTITY % counters "(i|I|1|a|A)"> <!ENTITY % counters "(i|I|1|a|A)">
<!ENTITY % special.extra "img">
<!ENTITY % special.basic "br | span | newpage"> <!ENTITY % special.basic "br | span | newpage">
<!ENTITY % special "%special.basic; | %special.extra;"> <!ENTITY % special "%special.basic;">
<!ENTITY % fontstyle.extra "big | small | font | basefont"> <!ENTITY % fontstyle.extra "big | small | font | basefont">
<!ENTITY % fontstyle.basic "tt | i | b | u | s | strike "> <!ENTITY % fontstyle.basic "tt | i | b | u | s | strike ">
<!ENTITY % fontstyle "%fontstyle.basic; | %fontstyle.extra;"> <!ENTITY % fontstyle "%fontstyle.basic; | %fontstyle.extra;">
@@ -45,6 +44,11 @@
<!ENTITY % key-value <!ENTITY % key-value
"key CDATA #REQUIRED "key CDATA #REQUIRED
value CDATA #REQUIRED"> value CDATA #REQUIRED">
<!ENTITY % imgAttrs
"src CDATA #IMPLIED
fromgame CDATA #IMPLIED
gameitem CDATA #IMPLIED
height %Number; #REQUIRED">
<!ENTITY % coreattrs <!ENTITY % coreattrs
"id CDATA #IMPLIED "id CDATA #IMPLIED
class CDATA #IMPLIED class CDATA #IMPLIED
@@ -55,7 +59,7 @@
<!ENTITY % heading "part | chapter | section | subsection"> <!ENTITY % heading "part | chapter | section | subsection">
<!ENTITY % lists "ul | ol"> <!ENTITY % lists "ul | ol">
<!ENTITY % blocktext "pre | anonbreak | callout | center | fullpagebox"> <!ENTITY % blocktext "pre | anonbreak | callout | center | fullpagebox">
<!ENTITY % block "p | %heading; | %lists; | %blocktext;"> <!ENTITY % block "p | %heading; | %lists; | %blocktext; | img">
<!ENTITY % Flow "(#PCDATA | %block; | %inline;)*"> <!ENTITY % Flow "(#PCDATA | %block; | %inline;)*">
<!ELEMENT btexdoc (macrodef?,cover?,tocpage?,manuscript,indexpage?)> <!ELEMENT btexdoc (macrodef?,cover?,tocpage?,manuscript,indexpage?)>
@@ -111,6 +115,8 @@
<!ATTLIST pair %key-value;> <!ATTLIST pair %key-value;>
<!ELEMENT index EMPTY> <!ELEMENT index EMPTY>
<!ATTLIST index %id-only;> <!ATTLIST index %id-only;>
<!ELEMENT img EMPTY>
<!ATTLIST img %imgAttrs;>
<!-- inherited from HTML --> <!-- inherited from HTML -->
<!ELEMENT p %Inline;> <!ELEMENT p %Inline;>

View File

@@ -63,6 +63,8 @@ class BTeXDocument : Disposable {
private fun String.escape() = this.replace("\"", "\\\"") private fun String.escape() = this.replace("\"", "\\\"")
private fun newTempFile(name: String) = FileHandle.tempFile(name)
fun fromFile(fileHandle: FileHandle) = fromFile(fileHandle.file()) fun fromFile(fileHandle: FileHandle) = fromFile(fileHandle.file())
fun fromFile(file: File): BTeXDocument { fun fromFile(file: File): BTeXDocument {
@@ -97,7 +99,7 @@ class BTeXDocument : Disposable {
Clustfile(DOM, "/${page}.png").also { Clustfile(DOM, "/${page}.png").also {
if (!it.exists()) throw IllegalStateException("No file '${page}.png' on the archive") if (!it.exists()) throw IllegalStateException("No file '${page}.png' on the archive")
val tempFile = Gdx.files.external("./.btex-import.png") // must create new file descriptor for every page, or else every page will share a single file descriptor which cause problems val tempFile = newTempFile("btex-import.png") // must create new file descriptor for every page, or else every page will share a single file descriptor which cause problems
it.exportFileTo(tempFile.file()) it.exportFileTo(tempFile.file())
val texture = TextureRegion(Texture(tempFile)) val texture = TextureRegion(Texture(tempFile))
doc.pageTextures[page] = texture doc.pageTextures[page] = texture
@@ -236,7 +238,7 @@ class BTeXDocument : Disposable {
pagePixmaps.forEachIndexed { index, pixmap -> pagePixmaps.forEachIndexed { index, pixmap ->
Clustfile(DOM, "$index.png").also { file -> Clustfile(DOM, "$index.png").also { file ->
file.createNewFile() file.createNewFile()
val tempFile = Gdx.files.external("./.btex-export.png") val tempFile = newTempFile("btex-export.png")
PixmapIO.writePNG(tempFile, pixmap, Deflater.BEST_COMPRESSION, false) PixmapIO.writePNG(tempFile, pixmap, Deflater.BEST_COMPRESSION, false)
val outstream = ClustfileOutputStream(file) val outstream = ClustfileOutputStream(file)
outstream.write(tempFile.readBytes()) outstream.write(tempFile.readBytes())

View File

@@ -1,10 +1,13 @@
package net.torvald.btex package net.torvald.btex
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.utils.Disposable import com.badlogic.gdx.utils.Disposable
import com.badlogic.gdx.utils.GdxRuntimeException
import com.jme3.math.FastMath.DEG_TO_RAD import com.jme3.math.FastMath.DEG_TO_RAD
import net.torvald.colourutil.OKLch import net.torvald.colourutil.OKLch
import net.torvald.colourutil.tosRGB import net.torvald.colourutil.tosRGB
@@ -27,15 +30,15 @@ import org.xml.sax.Attributes
import org.xml.sax.InputSource import org.xml.sax.InputSource
import org.xml.sax.SAXParseException import org.xml.sax.SAXParseException
import org.xml.sax.helpers.DefaultHandler import org.xml.sax.helpers.DefaultHandler
import java.io.File import java.io.*
import java.io.FileInputStream import java.net.URL
import java.io.StringReader
import javax.xml.parsers.SAXParserFactory import javax.xml.parsers.SAXParserFactory
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlin.reflect.full.declaredFunctions import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findAnnotation
/** /**
* Created by minjaesong on 2023-10-28. * Created by minjaesong on 2023-10-28.
*/ */
@@ -919,6 +922,74 @@ object BTeXParser {
paragraphBuffer.append(value) paragraphBuffer.append(value)
} }
@OpenTag // reflective access is impossible with 'private'
fun processElemIMG(handler: BTeXHandler, doc: BTeXDocument, uri: String, attribs: HashMap<String, String>) {
val heightInLines = attribs["height"]!!.toInt()
val imgHeight = doc.lineHeightInPx * heightInLines - 6
val btexObjName = "IMG@${makeRandomObjName()}"
val img = attribs["src"]
// image overflowing?
if (doc.pageLines - doc.linesPrintedOnPage.last() < heightInLines)
doc.addNewPage()
if (img != null) {
val tempFile = FileHandle.tempFile("btex_$btexObjName")
try {
val inputPixmap = if (img.startsWith("file://")) {
// printdbg("Using local file ${img.substring(7)}")
Pixmap(Gdx.files.absolute(img.substring(7)))
}
else {
// printdbg("Downloading image $img")
BufferedInputStream(URL(img).openStream()).use { `in` ->
FileOutputStream(tempFile.file()).use { fileOutputStream ->
val dataBuffer = ByteArray(1024)
var bytesRead: Int
while ((`in`.read(dataBuffer, 0, 1024).also { bytesRead = it }) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead)
}
}
}
Pixmap(tempFile).also { App.disposables.add(it) }
}
val imgWidth = (imgHeight.toFloat() / inputPixmap.height * inputPixmap.width).roundToInt()
if (imgWidth > doc.textWidth)
throw RuntimeException("Image width ($imgWidth) is larger than the text width (${doc.textWidth})")
val drawCallObj = { parentText: BTeXDrawCall -> object : BTeXBatchDrawCall(imgWidth, (heightInLines - 1).coerceAtLeast(0), parentText) {
private lateinit var inputTexture: Texture
override fun draw(doc: BTeXDocument, batch: SpriteBatch, x: Float, y: Float, font: TerrarumSansBitmap?) {
if (!::inputTexture.isInitialized) {
inputTexture = Texture(inputPixmap).also { App.disposables.add(it) }
}
val destX = (x + (doc.pageDimensionWidth - imgWidth) / 2)
val destY = y + 1
batch.draw(inputTexture, destX, destY, imgWidth.toFloat(), imgHeight.toFloat())
}
override fun drawToPixmap(doc: BTeXDocument, pixmap: Pixmap, x: Int, y: Int, font: TerrarumSansBitmap?) {
val destX = x
val destY = y + 1
pixmap.drawPixmap(inputPixmap, 0, 0, inputPixmap.width, inputPixmap.height, destX, destY, imgWidth, imgHeight)
}
} }
objDict[btexObjName] = { text: BTeXDrawCall -> drawCallObj(text) }
objWidthDict[btexObjName] = imgWidth
typesetParagraphs(objectMarkerWithWidth(btexObjName, imgWidth), handler, align = "center")
}
catch (e: IOException) { }
catch (e: GdxRuntimeException) { }
finally {
tempFile.delete()
}
}
}
@@ -1827,6 +1898,8 @@ object BTeXParser {
else -> throw IllegalArgumentException("Non-object ID char: $c") else -> throw IllegalArgumentException("Non-object ID char: $c")
} }
} }
private fun makeRandomObjName() = (0 until 12).joinToString("") { "${hashStrMap.random()}" }
} }
} }

View File

@@ -26,9 +26,9 @@ import kotlin.system.measureTimeMillis
*/ */
class BTeXTest : ApplicationAdapter() { class BTeXTest : ApplicationAdapter() {
val filePath = "btex.xml" // val filePath = "btex.xml"
// val filePath = "btex_ko.xml" // val filePath = "btex_ko.xml"
// val filePath = "test.xml" val filePath = "test.xml"
// val filePath = "literature/en/daniel_defoe_robinson_crusoe.xml" // val filePath = "literature/en/daniel_defoe_robinson_crusoe.xml"
// val filePath = "literature/ruRU/anton_chekhov_palata_no_6.xml" // val filePath = "literature/ruRU/anton_chekhov_palata_no_6.xml"
// val filePath = "literature/koKR/yisang_nalgae.btxbook" // val filePath = "literature/koKR/yisang_nalgae.btxbook"