Skip to main content

墨水序列化

Ink Serialization模块提供墨水内容及相关元数据的编码和解码算法。 这些算法经过优化,能够很好地与Universal Ink Model适应.

算法支持以下特性:

  • 快速编解码
  • 紧凑文件大小
  • 不同操作系统和设备间的可移植性
  • 可嵌入不同容器格式的二进制表示法

要阅读关于编码方案的更多信息,请单击here.

处理模型

The following illustrates how to manage the data used within the Universal Ink Model。 在墨水模型中,如果使用案例需要相关信息,则需要管理多个数据库 - 以及墨水树、视图和知识图谱。

Logical Parts of Ink Model.

Ink Model

第一步是创建InkModel.

inkModel = InkModel()

输入上下文数据库

输入上下文环境描述如何捕获传感器数据。 该信息需要由应用程序添加,但 -- 根据应用程序的使用案例 -- 也可能不需要。 传感器数据的存储也是可选的。

下列章节提供了如何定义上下文环境的一些实例。

环境

生成传感器数据(操作系统等)的environment可按照下列方式来定义:

import com.wacom.ink.format.input.*
...

// Initialize environment
environment = Environment()
environment.putProperty("os.name", "android")
environment.putProperty("os.version.name", Build.VERSION.CODENAME)
environment.putProperty("os.version.code", Build.VERSION.SDK_INT.toString())
environment.putProperty("os.version.incremental", Build.VERSION.INCREMENTAL)
environment.putProperty("os.version.release", Build.VERSION.RELEASE)
environment.putProperty("wacom.ink.sdk.name", activity.getString(R.string.sdk_name))
environment.putProperty("wacom.ink.sdk.version", activity.getString(R.string.sdk_version))

InkInputProvider

ink input provider 表示通用输入数据源。它标识数据的生成方式(使用触摸输入、鼠标、触控笔、硬件控制器等)。 对于每个平台和设备,应用程序必须确定它使用哪种输入设备,如何在墨水模型中序列化。

import com.wacom.ink.format.input.*
...
val toolType = when (event.getToolType(0)) {
MotionEvent.TOOL_TYPE_STYLUS -> InkInputType.PEN
MotionEvent.TOOL_TYPE_FINGER -> InkInputType.TOUCH
MotionEvent.TOOL_TYPE_MOUSE -> InkInputType.MOUSE
else -> InkInputType.PEN
}

val provider = InkInputProvider(toolType)

输入设备

input device 用于生成传感器数据。 根据所使用的平台,可能会有多种输入设备可用。 一些平板电脑提供触控笔,或者可作为附件提供。 下列属性可以根据需要进行收集,例如,由平台自身。

import com.wacom.ink.format.input.*
...

// Initialize InputDevice
inputDevice = InputDevice()
inputDevice.putProperty("dev.id", Build.ID)
inputDevice.putProperty("dev.manufacturer", Build.MANUFACTURER)
inputDevice.putProperty("dev.brand", Build.BRAND)
inputDevice.putProperty("dev.model", Build.MODEL)
inputDevice.putProperty("dev.board", Build.BOARD)
inputDevice.putProperty("dev.hardware", Build.HARDWARE)
inputDevice.putProperty("dev.codename", Build.DEVICE)
inputDevice.putProperty("dev.display", Build.DISPLAY)
...

SensorContext

sensor context定义了捕获数字墨水输入的传感器通道上下文环境的唯一组合。 对于在机器学习上下文环境中使用的传感器数据,如果需要标准化,该信息很有用。

// Define which channels are used for sensor data collection
var inkSensorTypes = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP)
...
// Register the channels which are used for sensor data collection
val channels = registerChannels(inkSensorTypeUris)

// Define the context for the sensor channels
val sensorChannelsContext = SensorChannelsContext(provider.id, inputDevice.id, channels)
val sensorContext = SensorContext()
sensorContext.addSensorChannelsContext(sensorChannelsContext)

// Maintain a map of contexts by id
sensorContexts[sensorContext.id] = sensorContext

// Input context is combining environment and sensor context
val inputContext = InputContext(environment.id, sensorContext.id)

// Maintain a map of input context
inputContexts[inputContext.id] = inputContext
inputProviderToInputContextMapping[provider.id] = inputContext.id

...
private fun registerChannels(inkSensorTypeUris: List<String>): MutableList<SensorChannel> {
val precision = 2

val channels = mutableListOf<SensorChannel>()

val dimensions = getScreenDimensions()

for (type in inkSensorTypeUris) {
val channel = when (type) {
InkSensorType.X -> SensorChannel(
InkSensorType.X,
InkSensorMetricType.LENGTH,
ScalarUnit.INCH,
0.0f,
dimensions.x,
precision
)
InkSensorType.Y -> SensorChannel(
InkSensorType.Y,
InkSensorMetricType.LENGTH,
ScalarUnit.INCH,
0.0f,
dimensions.y,
precision
)
InkSensorType.Z -> SensorChannel(
InkSensorType.Z,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.TIMESTAMP -> SensorChannel(
InkSensorType.TIMESTAMP,
InkSensorMetricType.TIME,
ScalarUnit.MILLISECOND,
0.0f,
0.0f,
precision
)
InkSensorType.PRESSURE -> SensorChannel(
InkSensorType.PRESSURE,
InkSensorMetricType.NORMALIZED,
ScalarUnit.NORMALIZED,
0.0f,
1.0f,
precision
)
InkSensorType.RADIUS_X -> SensorChannel(
InkSensorType.RADIUS_X,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.RADIUS_Y -> SensorChannel(
InkSensorType.RADIUS_Y,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.ALTITUDE -> SensorChannel(
InkSensorType.ALTITUDE,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
0.0f,
(Math.PI/2).toFloat(),
precision
)
InkSensorType.AZIMUTH -> SensorChannel(
InkSensorType.AZIMUTH,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
-(Math.PI/2).toFloat(),
(Math.PI/2).toFloat(),
precision
)
InkSensorType.ROTATION -> SensorChannel(
InkSensorType.ROTATION,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
0.0f,
0.0f,
precision
)
else -> {
throw Exception("Unknown channel type.")
}
}
channels.add(channel)
}
return channels
}

SensorData 版本库

要追踪传感器数据,在所有平台上都知道InkInputProvider很重要,便可从事件中推测出来。 而且,渲染特性也各不相同,例如,触摸和触控笔输入提供者,它们的渲染特性就不同。

...
val touchChannels = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP)
val penChannels = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP,
InkSensorType.PRESSURE, InkSensorType.ALTITUDE, InkSensorType.AZIMUTH)
...


fun createSensorData(event: MotionEvent): Pair<SensorData, List<SensorChannel>> {
// MotionEvent tool type helps to decide on the appropriate ink input provider
val toolType = when (event.getToolType(0)) {
MotionEvent.TOOL_TYPE_STYLUS -> InkInputType.PEN
MotionEvent.TOOL_TYPE_FINGER -> InkInputType.TOUCH
MotionEvent.TOOL_TYPE_MOUSE -> InkInputType.MOUSE
else -> InkInputType.PEN
}

var provider: InkInputProvider? = null
// First, check if exist an input provider of the desired type
for ((_, existingProvider) in inputProviders) {
existingProvider.type == toolType
provider = existingProvider
break
}

if (provider == null) {
provider = InkInputProvider(toolType)

// It is possible to add custom define properties to the input provider
// for example PenID in case exists
if (toolType == InkInputType.PEN) {
provider.putProperty("penType", "s-pen") // Assuming using Samsung S Pen
}
}

var inputContextId = if (!inputProviders.containsKey(provider.id)) {
inputProviders[provider.id] = provider

// Build the list of channels
val channels = registerChannels(if (toolType == InkInputType.PEN) penChannels else touchChannels)
channelsForInput.put(provider.id, channels)

val sensorChannelsContext = SensorChannelsContext(
provider.id, // Reference to input input provider
inputDevice.id, // Reference to input device
channels) // Channels for the registered channels

val sensorContext = SensorContext()
sensorContext.addSensorChannelsContext(sensorChannelsContext)

sensorContexts[sensorContext.id] = sensorContext

val inputContext = InputContext(
environment.id, // Reference to environment
sensorContext.id) // Reference to sensor context

inputContexts[inputContext.id] = inputContext

inputProviderToInputContextMapping[provider.id] = inputContext.id

inputContext.id
} else {
val provider = inputProviders[provider.id]!!
inputProviderToInputContextMapping[provider.id]!!
}
var channelList: List<SensorChannel>? = null
for ((id, channels) in channelsForInput) {
if (provider.id == id) {
channelList = channels
}
}

if (channelList == null) {
channelList = listOf() // empty list to avoid null pointer exceptions
}
// Create the SensorData object with a new unique ID and its input context reference id
return Pair<SensorData,
List<SensorChannel>>(SensorData(UUID.randomUUID().toString(), inputContextId, InkState.PLANE),
channelList)
}


override fun onEvent(pointerData: PointerData, inkToolType: InkInputType) {
when (pointerData.phase) {
// Set the appropriate input provider
Phase.BEGIN -> {
if ((inputProvider.type != inkToolType)) {
inputProvider = InkInputProvider(inkToolType)
}
}
// Adding sensor data
Phase.UPDATE, Phase.END -> {
sensorData.add(sensorChannels[InkSensorType.X]!!, pointerData.x)
sensorData.add(sensorChannels[InkSensorType.Y]!!, pointerData.y)
sensorData.addTimestamp(sensorChannels[InkSensorType.TIMESTAMP]!!,
pointerData.timestamp)
}
}
}
...
}

Stroke 版本库

rendering pipeline的视觉输出也必须保存,它是通用墨水模型的关键部分。 在此,需要添加下列结构:

  • Stroke - 墨水笔划的几何结构
  • Style - 笔划渲染的定制参数
  • StrokeNode - 墨水树的逻辑节点
import com.wacom.ink.format.rendering.PathPointProperties
import com.wacom.ink.format.rendering.Style
import com.wacom.ink.format.tree.data.Stroke
import com.wacom.ink.format.tree.nodes.StrokeNode
import com.wacom.ink.model.IdentifiableImpl

...
fun surfaceTouch(event: MotionEvent) {
...
if ((pointerData.phase == Phase.END) &&
(rasterInkBuilder.splineProducer.allData != null)) {
addStroke(event)
}
...
}

...
private fun addStroke(event: MotionEvent) {
// Adding the style
val style = Style(
rasterTool.uri(), // Style URI
1, // Particle random seed
props = PathPointProperties( // Coloring path properties
red = defaults.red,
green = defaults.green,
blue = defaults.blue,
alpha = defaults.alpha
),
renderModeUri = rasterTool.getBlendMode().name
)
// Adding stroke to the Stroke Repository
val path = Stroke(
IdentifiableImpl.generateUUID(), // Generated UUID
rasterInkBuilder.splineProducer.allData!!.copy(), // Spline
style // Style
)
// Adding a node to the Ink tree
val node = StrokeNode(IdentifiableImpl.generateUUID(),
path)
...
strokeNodeList.add(node)
}

Brush 版本库

由于需要确保墨水笔划可在不同平台或应用程序上进行渲染,每种刷子都必须是嵌入式的。

fun setTool(view: View, tool: Tool) {
drawingTool = tool

if (drawingTool is VectorTool) {
vectorDrawingView.setTool(drawingTool as VectorTool)
} else {
val dt = drawingTool as RasterTool
val brush = dt.brush
if (inkModel.brushRepository.getBrush(brush.name) == null) {
// Adding a raster brush if it is not within the repository yet
inkModel.brushRepository.addRasterBrush(brush as RasterBrush)
}
rasterDrawingSurface.setTool(dt)
}
highlightTool(view)
}

暂留

为确保墨水模型的一致性以及可跨平台分享,需要将其编码到数据流中。

编码

模型将使用二进制表达式表面,更多细节请参见 here.

try {
val file = File(filePath)
fileOutputStream = FileOutputStream(file)
// Serialize encoded ink model to an output stream
Will3Codec.encode(inkModel, fileOutputStream)
} catch (e: Exception){
e.printStackTrace()
} finally {
fileOutputStream?.close()
}

解码

为解码数据流,需要将编码后的数据传送到解码器中。 由于数据格式的版本号编码在RIFF标题中,将自动选择合适的内容解析器版本。

// Loading file stream from path
val fileInputStream = FileInputStream(File(path))
// Read bytes array with UIM content
val bytes = fileInputStream.readBytes()
// Using the codec decoder to de-serialize the InkModel
val inkModel = Will3Codec.decode(bytes)