Verified Commit cb810d61 authored by Soren's avatar Soren

Convert ByteAllocator to an immutable style

parent 3a2ee30e
......@@ -5,6 +5,8 @@ lazy val commonSettings = Seq(
organization := "tf.bug",
batikVersion := "1.10",
imageIOVersion := "3.4.1",
scalacOptions += "-Ypartial-unification",
resolvers += Resolver.bintrayRepo("alexknvl", "maven"),
)
lazy val core = (project in file(".")).settings(
......@@ -13,18 +15,22 @@ lazy val core = (project in file(".")).settings(
version := "0.1.0",
scalaVersion := "2.12.8",
libraryDependencies ++= Seq(
"org.typelevel" %% "spire" % "0.16.0",
"org.apache.xmlgraphics" % "batik-svg-dom" % batikVersion.value,
"org.apache.xmlgraphics" % "batik-transcoder" % batikVersion.value,
"org.apache.xmlgraphics" % "batik-extension" % batikVersion.value,
"org.apache.xmlgraphics" % "batik-rasterizer-ext" % batikVersion.value,
"com.twelvemonkeys.imageio" % "imageio-batik" % imageIOVersion.value,
"com.twelvemonkeys.imageio" % "imageio-core" % imageIOVersion.value,
"com.twelvemonkeys.imageio" % "imageio-metadata" % imageIOVersion.value,
"com.twelvemonkeys.common" % "common-lang" % imageIOVersion.value,
"org.scalatest" %% "scalatest" % "3.0.5" % "test",
"com.storm-enroute" %% "scalameter" % "0.10" % "test",
"org.scodec" %% "scodec-bits" % "1.1.6",
"org.typelevel" %% "spire" % "0.16.0",
"org.typelevel" %% "cats-core" % "1.5.0",
"org.scodec" %% "scodec-bits" % "1.1.6",
"com.alexknvl" %% "polymorphic" % "0.4.0",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.0",
"org.apache.xmlgraphics" % "batik-svg-dom" % batikVersion.value,
"org.apache.xmlgraphics" % "batik-transcoder" % batikVersion.value,
"org.apache.xmlgraphics" % "batik-extension" % batikVersion.value,
"org.apache.xmlgraphics" % "batik-rasterizer-ext" % batikVersion.value,
"com.twelvemonkeys.imageio" % "imageio-batik" % imageIOVersion.value,
"com.twelvemonkeys.imageio" % "imageio-core" % imageIOVersion.value,
"com.twelvemonkeys.imageio" % "imageio-metadata" % imageIOVersion.value,
"com.twelvemonkeys.common" % "common-lang" % imageIOVersion.value,
"ch.qos.logback" % "logback-classic" % "1.2.3",
"org.scalatest" %% "scalatest" % "3.0.5" % "test",
"com.storm-enroute" %% "scalameter" % "0.10" % "test",
),
testFrameworks += new TestFramework("org.scalameter.ScalaMeterFramework"),
logBuffered := false,
......
package tf.bug.scolor
import spire.math.{UByte, UInt}
import cats.data.State
import scodec.bits.ByteVector
import spire.math.UInt
abstract class ByteAllocator {
trait ByteAllocator {
/**
* Insert data at an offset in the byte allocator.
......@@ -10,16 +12,22 @@ abstract class ByteAllocator {
* @param offset The offset
* @param data The data
*/
def insert(offset: Offset, data: Data): Unit
def insert[T](offset: Offset, data: T)(implicit datableEv: Datable[T]): ByteAllocator
/**
* Insert data at the next available offset.
*
* @param data The data
*/
def insert(data: Data): Unit = insert(allocate(data), data)
def insert[T](data: T)(implicit datableEv: Datable[T]): ByteAllocator = {
val (newBA, allocOffset) = allocate(data)
newBA.insert(allocOffset, data)
}
def allocate(data: Data): Offset = allocate(data, data.length(this))
def allocate[T](data: T)(implicit datableEv: Datable[T]): (ByteAllocator, Offset) = {
val (endBA, endL) = datableEv.length(data).run(this).value
endBA.allocate(data, endL)
}
/**
* Allocate an offset for a number of bytes.
......@@ -27,12 +35,12 @@ abstract class ByteAllocator {
* @param numBytes The number of bytes to allocate
* @return an offset describing the next open space these bytes can fit
*/
def allocate(numBytes: UInt): Offset
def allocate(numBytes: UInt): (ByteAllocator, Offset)
def allocate(data: Data, numBytes: UInt): Offset
def allocate[T](data: T, numBytes: UInt)(implicit datableEv: Datable[T]): (ByteAllocator, Offset)
def nextOffset: Offset
def getBytes: Array[UByte]
def getBytes: ByteVector
}
package tf.bug.scolor
import cats.data.State
import scodec.bits.ByteVector
import spire.math.{UByte, UInt}
trait Data {
......@@ -7,10 +9,9 @@ trait Data {
/**
* Calculate/retrieve/return length in bytes of this data. Useful for if data needs to be allocated before it is calculated.
*
* @param b The byte allocator
* @return an unsigned integer describing the length of this data block
*/
def length(b: ByteAllocator): UInt
def length: State[ByteAllocator, UInt]
/**
* @return what byte modulus this data should be aligned to
......@@ -20,17 +21,15 @@ trait Data {
/**
* Get the bytes to insert at the offset the byte allocator gives you.
*
* @param b The byte allocator
* @return an array of unsigned bytes representing the font data.
* @return a byte vector representing the font data.
*/
def bytes(b: ByteAllocator): Array[UByte]
def bytes: State[ByteAllocator, ByteVector]
/**
* Gets data sections if this data block has offsets. Used for if data needs to be allocated but can be in any location.
*
* @param b The byte allocator
* @return an array of Data objects
* @return a collection of Data objects
*/
def data(b: ByteAllocator): Traversable[Data]
def data: State[ByteAllocator, List[Data]]
}
package tf.bug.scolor
import tf.bug.scolor.implicits._
import cats.data.State
import polymorphic.Instance
import scodec.bits.ByteVector
import spire.math.{UByte, UInt}
trait Datable[-T] {
def alignment(t: T): UByte
def children(t: T): BAW[List[Instance[Datable]]]
def byteVector(t: T): BAW[ByteVector]
def length(t: T): BAW[UInt]
}
trait DefiniteByteable[-T] extends Datable[T] {
override def alignment(t: T): UByte = UByte(1)
override def children(t: T): BAW[List[Instance[Datable]]] = State(i => (i, List()))
override def length(t: T): BAW[UInt] = byteVector(t).map(l => UInt(l.length))
}
......@@ -2,6 +2,7 @@ package tf.bug.scolor
import java.io.File
import scodec.bits.ByteVector
import spire.math.UByte
/**
......@@ -12,7 +13,7 @@ trait Font {
/**
* @return The bytes of the font file
*/
def getBytes: Array[UByte]
def getBytes: ByteVector
/**
* Write a font/set of fonts to a directory. Multiple are allowed for different platforms or styles.
......
......@@ -21,9 +21,9 @@ case class ColorEmojiFont(
created: Long,
entries: Map[Codepoint, ColorEmojiEntry]
) extends OpenTypeFont(
Seq(
List(
OTFNameTable(
Seq(
List(
OTFNameRecord(
d = WindowsLanguage.`English`.`United States`,
content = internationalName
......@@ -106,12 +106,12 @@ case class ColorEmojiFont(
UShort(0)
),
OTFCMapTable(
Seq(
List(
OTFEncodingRecord(
UShort(3),
UShort(1),
OTFEncodingRecord.SegmentedCoverageEncodingFormat(
entries.keys.zipWithIndex.map {
entries.keys.toList.zipWithIndex.map {
case (c, i) =>
SequentialMapGroup(c, c, UInt(i + 1))
}
......@@ -121,7 +121,7 @@ case class ColorEmojiFont(
UShort(1),
UShort(0),
OTFEncodingRecord.SegmentedCoverageEncodingFormat(
entries.keys.zipWithIndex.map {
entries.keys.toList.zipWithIndex.map {
case (c, i) =>
SequentialMapGroup(c, c, UInt(i + 1))
}
......@@ -133,17 +133,17 @@ case class ColorEmojiFont(
UShort(entries.size)
),
OTFCBDTTable(
entries.values.flatMap(_.bitmaps.values.map { case (google, _) => google })
entries.values.toList.flatMap(_.bitmaps.values.toList.map { case (google, _) => google })
),
OTFSBIXTable(
Seq(OTFAppleStrikeData(pixelsPerEm, UShort(300), entries.values.flatMap(_.bitmaps.values.map {
List(OTFAppleStrikeData(pixelsPerEm, UShort(300), entries.values.toList.flatMap(_.bitmaps.values.toList.map {
case (_, apple) => apple
})))
),
OTFSVGTable(
OTFSVGDocumentIndex(
entries.values.flatMap { entry =>
entry.toScalable.values.zipWithIndex.map {
entries.values.toList.flatMap { entry =>
entry.toScalable.values.toList.zipWithIndex.map {
case (doc, index) =>
OTFSVGDocumentIndexEntry(
OTFUInt16(UShort(index)),
......
package tf.bug.scolor
import cats.data.State
import cats.kernel.Monoid
import polymorphic.Instance
import scodec.bits.ByteVector
import spire.math.{UByte, UInt, UShort}
object implicits {
type BAW[A] = State[ByteAllocator, A]
implicit val byteVectorMonoid: Monoid[ByteVector] = new Monoid[ByteVector] {
override def empty: ByteVector = ByteVector.empty
override def combine(
x: ByteVector,
y: ByteVector
): ByteVector = x ++ y
}
implicit val uintMonoid: Monoid[UInt] = new Monoid[UInt] {
override def empty: UInt = UInt(0)
override def combine(x: UInt, y: UInt): UInt = x + y
}
implicit def datableToData[T](t: T)(implicit datableEv: Datable[T]): Data = new Data {
override def length: BAW[UInt] = datableEv.length(t)
override def bytes: BAW[ByteVector] = datableEv.byteVector(t)
override def data: BAW[List[Data]] = datableEv.children(t).map(_.map(c => datableToData(c.first)(c.second)))
override def alignment: UByte = datableEv.alignment(t)
}
implicit def dataDatable: Datable[Data] = new Datable[Data] {
override def alignment(t: Data): UByte = t.alignment
override def children(t: Data): BAW[List[Instance[Datable]]] =
t.data.map(_.map((d: Data) => Instance.capture(d)(dataDatable)))
override def byteVector(t: Data): BAW[ByteVector] = t.bytes
override def length(t: Data): BAW[UInt] = t.length
}
implicit def stringByteable: DefiniteByteable[String] =
(t: String) => State(i => (i, ByteVector(t.getBytes)))
implicit def uShortByteable: DefiniteByteable[UShort] =
(t: UShort) => State(i => (i, ByteVector(((t.toShort & 0xFF00) >> 8).toByte, (t.toShort & 0x00FF).toByte)))
implicit def uIntBytable: DefiniteByteable[UInt] =
(t: UInt) =>
State(
i =>
(
i,
ByteVector(
((t.toInt & 0xFF000000) >> 24).toByte,
((t.toInt & 0x00FF0000) >> 16).toByte,
((t.toInt & 0x0000FF00) >> 8).toByte,
(t.toInt & 0x000000FF).toByte
)
)
)
implicit def byteBytable: DefiniteByteable[Byte] = (t: Byte) => State(i => (i, ByteVector(t)))
def allocate[T: Datable](t: T): BAW[Offset] = State(b => b.allocate(t))
}
package tf.bug.scolor.otf
import tf.bug.scolor.otf.types.OTFOffset32
import tf.bug.scolor.{ByteAllocator, Data, Offset}
import polymorphic.Instance
import scodec.bits.ByteVector
import spire.math.{UByte, UInt}
import scala.collection.mutable
class OTFByteAllocator(f: OpenTypeFont) extends ByteAllocator {
private val allocMap: mutable.Map[Data, Offset] = mutable.Map()
private val byteMap: mutable.Map[Offset, UByte] = mutable.Map()
private var nextAvailableOffset: Offset = OTFOffset32(0)
def insert(offset: Offset, data: Data): Unit = {
val bytes = data.bytes(this)
import tf.bug.scolor.implicits._
import tf.bug.scolor.otf.types.OTFOffset32
import tf.bug.scolor.{ByteAllocator, Datable, Offset}
case class OTFByteAllocator(
f: OpenTypeFont,
allocMap: Map[Instance[Datable], Offset] = Map(),
byteMap: Map[Offset, UByte] = Map(),
nextAvailableOffset: Offset = OTFOffset32(0)
) extends ByteAllocator {
def insert[T](offset: Offset, data: T)(implicit datableEv: Datable[T]): ByteAllocator = {
val (newAlloc @ OTFByteAllocator(_, _, newByteMap, _), bytes) = datableEv.byteVector(data).run(this).value
val alignment = datableEv.alignment(data)
val top = offset.position.toInt + bytes.length
(offset.position.toInt until top).foreach(p => byteMap += (OTFOffset32(p) -> bytes(p - offset.position.toInt)))
nextAvailableOffset = OTFOffset32(
Math.max(top + (data.alignment.toInt - (top % data.alignment.toInt)), nextOffset.position.toLong)
val newBM = (offset.position.toLong until top)
.foldLeft(newByteMap)((nm, p) => nm + (OTFOffset32(p) -> UByte(bytes(p - offset.position.toLong))))
val adjustedOffset = OTFOffset32(
Math.max(top + (alignment.toInt - (top % alignment.toInt)), newAlloc.nextOffset.position.toLong)
)
data.data(this).foreach(insert)
val newBA = newAlloc.copy(byteMap = newBM, nextAvailableOffset = adjustedOffset)
val (endBA, bv) = datableEv.byteVector(data).run(newBA).value
bv.foldLeft(endBA)((na, d) => na.insert(d))
}
def nextOffset: Offset = nextAvailableOffset
def allocate(data: Data, numBytes: UInt): Offset = {
allocMap.getOrElseUpdate(data, allocate(numBytes))
def allocate[T](data: T, numBytes: UInt)(implicit datableEv: Datable[T]): (ByteAllocator, Offset) = {
allocMap.get(data) match {
case Some(o) => (this, o)
case None => allocate(numBytes)
}
}
def allocate(numBytes: UInt): Offset = {
def allocate(numBytes: UInt): (ByteAllocator, Offset) = {
allocate(numBytes, UByte(1))
}
def allocate(numBytes: UInt, alignment: UByte): Offset = {
def allocate(numBytes: UInt, alignment: UByte): (ByteAllocator, Offset) = {
val prev = nextOffset
val unaligned = prev.position + numBytes
nextAvailableOffset = OTFOffset32(unaligned.toLong + (alignment.toLong - (unaligned.toLong % alignment.toLong)))
prev
val adjOffset = OTFOffset32(unaligned.toLong + (alignment.toLong - (unaligned.toLong % alignment.toLong)))
(copy(nextAvailableOffset = adjOffset), prev)
}
override def allocate(data: Data): Offset = {
val allocation = super.allocate(data)
allocation
}
override def nextOffset: Offset = nextAvailableOffset
def getBytes: Array[UByte] = {
def getBytes: ByteVector = {
val m: Long = byteMap.map { case (offset, _) => offset.position.toLong }.max
(0l to m).map(i => byteMap.getOrElse(OTFOffset32(i), UByte(0))).toArray
ByteVector((0l to m).map(i => byteMap.getOrElse(OTFOffset32(i), UByte(0)).signed))
}
}
object OTFByteAllocator {}
package tf.bug.scolor.otf
import java.io.{File, FileOutputStream}
import java.nio.{ByteBuffer, ByteOrder}
import java.io.File
import tf.bug.scolor._
import tf.bug.scolor.otf.tables.OTFHeadTable
import tf.bug.scolor.otf.types.OTFOffset32
import scodec.bits.ByteVector
import tf.bug.scolor.Font
import tf.bug.scolor.table.Table
import spire.math.{UByte, UInt, UShort}
import spire.syntax.std.array._
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
import scala.language.implicitConversions
class OpenTypeFont(tables: Traversable[Table]) extends Font {
private val tableOffsMap: mutable.Map[Table, Offset] = mutable.Map()
implicit def byteArrayToData(ba: Array[UByte]): Data = new Data {
/**
* Gets data sections if this data block has offsets
*
* @param b The byte allocator
* @return an array of Data objects
*/
override def data(b: ByteAllocator): Traversable[Data] = Seq()
override def length(b: ByteAllocator): UInt = UInt(ba.length)
override def bytes(b: ByteAllocator): Array[UByte] = ba
}
def writeDataRecursively(b: ByteAllocator, d: Data): Unit = {
b.insert(d)
d.data(b).foreach(c => writeDataRecursively(b, c))
}
def writeTables(b: ByteAllocator): Unit = {
tableOffsMap.foreach {
case (table, _) => writeDataRecursively(b, table)
}
}
def writeHeader(b: ByteAllocator): Unit = {
def mPow2Shifts(i: Int): Int = {
Stream.from(0).takeWhile(shifts => (i & (Int.MaxValue << shifts)) != 0).last
}
def mPow2Less(i: Int): Int = {
1 << (mPow2Shifts(i) - 1)
}
b.allocate(UInt(12))
val buff = ListBuffer[UByte]()
buff ++= "OTTO".getBytes.map(UByte(_))
buff ++= UShort(tables.size).bytes
val searchRange = mPow2Less(tables.size) * 16
buff ++= UShort(searchRange).bytes
buff ++= UShort(mPow2Shifts(tables.size)).bytes
buff ++= UShort(tables.size * 16 - searchRange).bytes
b.allocate(UInt(tables.size * 16))
tables.foreach(t => {
buff ++= t.name.getBytes.map(UByte(_))
val dataOffset = b.allocate(t)
tableOffsMap += (t -> dataOffset)
val bytes = t.bytes(b)
buff ++= bytes.checksum.bytes
buff ++= dataOffset.position.bytes
buff ++= t.length(b).bytes
})
b.insert(OTFOffset32(0), buff.toArray)
}
def writeHeaderSecondPass(b: ByteAllocator, newHead: OTFHeadTable): Unit = {
tableOffsMap.clear()
def mPow2Shifts(i: Int): Int = {
Stream.from(0).takeWhile(shifts => (i & (Int.MaxValue << shifts)) != 0).last
}
def mPow2Less(i: Int): Int = {
1 << (mPow2Shifts(i) - 1)
}
b.allocate(UInt(12))
val buff = ListBuffer[UByte]()
buff ++= "OTTO".getBytes.map(UByte(_))
buff ++= UShort(tables.size).bytes
val searchRange = mPow2Less(tables.size) * 16
buff ++= UShort(searchRange).bytes
buff ++= UShort(mPow2Shifts(tables.size)).bytes
buff ++= UShort(tables.size * 16 - searchRange).bytes
b.allocate(UInt(tables.size * 16))
tables.foreach(t => {
if (t.name == "head") {
buff ++= newHead.name.getBytes.map(UByte(_))
val dataOffset = b.allocate(newHead)
tableOffsMap += (newHead -> dataOffset)
buff ++= t.bytes(b).checksum.bytes
buff ++= dataOffset.position.bytes
buff ++= newHead.length(b).bytes
} else {
buff ++= t.name.getBytes.map(UByte(_))
val dataOffset = b.allocate(t)
tableOffsMap += (t -> dataOffset)
buff ++= t.bytes(b).checksum.bytes
buff ++= dataOffset.position.bytes
buff ++= t.length(b).bytes
}
})
b.insert(OTFOffset32(0), buff.toArray)
}
def writeTablesSecondPass(b: ByteAllocator, newHead: OTFHeadTable): Unit = {
tableOffsMap.foreach {
case (table, _) =>
if (table.name == "head") {
writeDataRecursively(b, newHead)
} else {
writeDataRecursively(b, table)
}
}
}
override def writeFile(dir: File, name: String): Unit = {
dir.mkdirs()
val `Windows Font Tables` = tables.filter(t => !Seq("cbdt", "cblc").contains(t.name.toLowerCase))
val `Mac Font Tables` = tables.filter(t => !Seq("colr", "cpal", "cbdt", "cblc").contains(t.name.toLowerCase))
val `Linux Font Tables` = tables.filter(t => !Seq("sbix", "colr", "cpal").contains(t.name.toLowerCase))
val winFont = new OpenTypeFont(`Windows Font Tables`)
val macFont = new OpenTypeFont(`Mac Font Tables`)
val nixFont = new OpenTypeFont(`Linux Font Tables`)
safeCreateAndWrite(new File(dir, name + ".otf"), getBytes)
safeCreateAndWrite(new File(dir, name + "-win.otf"), winFont.getBytes)
safeCreateAndWrite(new File(dir, name + "-mac.otf"), macFont.getBytes)
safeCreateAndWrite(new File(dir, name + "-nix.otf"), nixFont.getBytes)
}
implicit class OpenTypeTable(bytes: Array[UByte]) {
def checksum: UInt = {
val intBuf = ByteBuffer.wrap(bytes.map(_.signed)).order(ByteOrder.BIG_ENDIAN).asIntBuffer
val ints = (bytes.length + 3) / 4 - 1
(0 until ints).map(i => UInt(intBuf.get(i))).toArray.qsum
}
}
implicit class BytableUShort(s: UShort) {
def bytes: Array[UByte] = {
Array(((s.toShort & 0xFF00) >> 8).toByte, (s.toShort & 0x00FF).toByte).map(UByte(_))
}
}
implicit class BytableUInt(i: UInt) {
def bytes: Array[UByte] = {
Array(
((i.toInt & 0xFF000000) >> 24).toByte,
((i.toInt & 0x00FF0000) >> 16).toByte,
((i.toInt & 0x0000FF00) >> 8).toByte,
(i.toInt & 0x000000FF).toByte
).map(UByte(_))
}
}
override def getBytes: Array[UByte] = {
val b: ByteAllocator = new OTFByteAllocator(this)
writeHeader(b)
writeTables(b)
tables.find(_.name == "head") match {
case Some(t) =>
val nb: ByteAllocator = new OTFByteAllocator(this)
val newHead =
t.asInstanceOf[OTFHeadTable].copy(checkSumAdjustment = UInt(0xB1B0AFBA - b.getBytes.checksum.signed))
writeHeaderSecondPass(nb, newHead)
writeTablesSecondPass(nb, newHead)
nb.getBytes
case None => b.getBytes
}
}
/** Writes bytes to a file without leaving resources open on failure */
private def safeCreateAndWrite(file: File, uBytes: => Array[UByte]): Unit = {
file.createNewFile()
val fileOutputStream = new FileOutputStream(file, false)
try {
fileOutputStream.write(uBytes.map(_.toByte))
} finally {
fileOutputStream.close()
}
}
// TODO reimplement this functionally
class OpenTypeFont(tables: List[Table]) extends Font {
/**
* @return The bytes of the font file
*/
override def getBytes: ByteVector = ???
/**
* Write a font/set of fonts to a directory. Multiple are allowed for different platforms or styles.
*
* @param dir The directory
* @param name The base name of the font
*/
override def writeFile(dir: File, name: String): Unit = ???
}
package tf.bug.scolor.otf.tables
import cats._
import cats.data._
import cats.implicits._
import tf.bug.scolor.implicits._
import tf.bug.scolor.{ByteAllocator, Data}
import tf.bug.scolor.otf.types.num.OTFUInt16
import tf.bug.scolor.otf.types.{OTFArray, OTFEncodingRecord, TabledEncodingRecord}
......@@ -7,31 +11,34 @@ import tf.bug.scolor.table.Section
import spire.math.{UInt, UShort}
case class OTFCMapTable(
encodingRecords: Traversable[OTFEncodingRecord]
encodingRecords: List[OTFEncodingRecord]
) extends OpenTypeTable {
private val tabledRecords: Traversable[TabledEncodingRecord] = encodingRecords.map(_(this))
private val tabledRecords: List[TabledEncodingRecord] = encodingRecords.map(_(this))
private val recArr: OTFArray[TabledEncodingRecord] = OTFArray(tabledRecords)
override def name = "cmap"
override def length(b: ByteAllocator): UInt = {
sections(b).foldLeft(UInt(0)) {
case (accum, section) => accum + section.data.length(b)
}
}
override def sections(b: ByteAllocator): Traversable[Section] = Seq(