use crate::binary::{Reader, Writer};
use crate::error::{Result, bail};
pub mod ids {
pub const RUN_FONT_SIZE: u32 = 11;
pub const FONT_SIZE: u32 = 32;
pub const BBOX: u32 = 42;
pub const BALLOON: u32 = 45;
pub const RENDER_OFFSCREEN: u32 = 50;
pub const FONT: u32 = 57;
pub const BLOCK_SIZE: u32 = 63;
pub const QUAD: u32 = 64;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
pub id: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Attributes {
pub records: Vec<Record>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Font {
pub lead: u16,
pub name: String,
pub postscript_name: String,
}
impl Attributes {
pub fn parse(blob: &[u8]) -> Result<Self> {
let mut reader = Reader::new(blob);
let mut records = Vec::new();
while !reader.is_empty() {
let id = reader.le_u32()?;
let length = reader.le_u32()? as usize;
if length > reader.remaining() {
bail!("Text attribute {id} says {length} bytes, {} remain", reader.remaining());
}
records.push(Record { id, data: reader.bytes(length)?.to_vec() });
}
Ok(Attributes { records })
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut writer = Writer::new();
for record in &self.records {
writer.le_u32(record.id).le_u32(record.data.len() as u32).bytes(&record.data);
}
writer.into_inner()
}
pub fn get(&self, id: u32) -> Option<&[u8]> {
self.records.iter().find(|record| record.id == id).map(|record| record.data.as_slice())
}
pub fn set(&mut self, id: u32, data: Vec<u8>) {
match self.records.iter_mut().find(|record| record.id == id) {
Some(record) => record.data = data,
None => self.records.push(Record { id, data }),
}
}
fn i32s(&self, id: u32) -> Option<Vec<i32>> {
let data = self.get(id)?;
Some(data.as_chunks::<4>().0.iter().map(|b| i32::from_le_bytes(*b)).collect())
}
fn set_i32s(&mut self, id: u32, values: &[i32]) {
self.set(id, values.iter().flat_map(|value| value.to_le_bytes()).collect());
}
pub fn font_size(&self) -> Option<f64> {
self.i32s(ids::FONT_SIZE)?.first().map(|value| *value as f64 / 100.0)
}
pub fn bbox(&self) -> Option<[i32; 4]> {
self.i32s(ids::BBOX)?.try_into().ok()
}
pub fn set_bbox(&mut self, bbox: [i32; 4]) {
self.set_i32s(ids::BBOX, &bbox);
}
pub fn block_size(&self) -> Option<[i32; 2]> {
self.i32s(ids::BLOCK_SIZE)?.try_into().ok()
}
pub fn quad(&self) -> Option<[i32; 8]> {
self.i32s(ids::QUAD)?.try_into().ok()
}
pub fn balloon(&self) -> Option<u32> {
self.i32s(ids::BALLOON)?.first().map(|value| *value as u32).filter(|value| *value > 0)
}
pub fn render_offscreen(&self) -> Option<u32> {
self.i32s(ids::RENDER_OFFSCREEN)?.first().map(|value| *value as u32)
}
pub fn clear_render_offscreen(&mut self) {
if self.get(ids::RENDER_OFFSCREEN).is_some() {
self.set_i32s(ids::RENDER_OFFSCREEN, &[0]);
}
}
pub fn font(&self) -> Option<Font> {
let data = self.get(ids::FONT)?;
let mut reader = Reader::new(data);
let lead = reader.le_u16().ok()?;
let length = reader.le_u16().ok()? as usize;
let name = String::from_utf8_lossy(reader.bytes(length).ok()?).into_owned();
let length = reader.le_u16().ok()? as usize;
let postscript_name = String::from_utf8_lossy(reader.bytes(length).ok()?).into_owned();
Some(Font { lead, name, postscript_name })
}
pub fn transform(&mut self, scale: f64, dx: f64, dy: f64) {
if let Some(b) = self.bbox() {
self.set_bbox([
(b[0] as f64 * scale + dx).floor() as i32,
(b[1] as f64 * scale + dy).floor() as i32,
(b[2] as f64 * scale + dx).ceil() as i32,
(b[3] as f64 * scale + dy).ceil() as i32,
]);
}
if let Some(size) = self.block_size() {
self.set_i32s(ids::BLOCK_SIZE, &size.map(|v| (v as f64 * scale).round() as i32));
}
if let Some(quad) = self.quad() {
self.set_i32s(ids::QUAD, &quad.map(|v| (v as f64 * scale).round() as i32));
}
}
}
pub fn split_array(blob: &[u8]) -> Result<Vec<&[u8]>> {
let mut reader = Reader::new(blob);
let mut parts = Vec::new();
while !reader.is_empty() {
let length = reader.le_u32()? as usize;
if length > reader.remaining() {
bail!("Array entry of {length} bytes, {} remain", reader.remaining());
}
parts.push(reader.bytes(length)?);
}
Ok(parts)
}
pub fn join_array<T: AsRef<[u8]>>(parts: &[T]) -> Vec<u8> {
let mut writer = Writer::new();
for part in parts {
writer.le_u32(part.as_ref().len() as u32).bytes(part.as_ref());
}
writer.into_inner()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextBlock {
pub text: String,
pub attributes: Attributes,
}
pub fn read_blocks(row: &crate::database::Row) -> Result<Vec<TextBlock>> {
let mut blocks = Vec::new();
let (text, attributes) = (row.blob("TextLayerString"), row.blob("TextLayerAttributes"));
if text.is_some() || attributes.is_some() {
blocks.push(block(text.unwrap_or_default(), attributes)?);
}
let strings = row.blob("TextLayerStringArray").map(split_array).transpose()?.unwrap_or_default();
let attributes = row.blob("TextLayerAttributesArray").map(split_array).transpose()?.unwrap_or_default();
for index in 0..strings.len().max(attributes.len()) {
blocks.push(block(strings.get(index).copied().unwrap_or_default(), attributes.get(index).copied())?);
}
Ok(blocks)
}
fn block(text: &[u8], attributes: Option<&[u8]>) -> Result<TextBlock> {
let attributes = match attributes {
Some(blob) => Attributes::parse(blob)?,
None => Attributes::default(),
};
Ok(TextBlock { text: String::from_utf8_lossy(text).into_owned(), attributes })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attributes_round_trip_and_decode() {
let mut attributes = Attributes::default();
attributes.set(ids::FONT_SIZE, 600i32.to_le_bytes().to_vec());
attributes.set_bbox([1, 2, 30, 40]);
attributes.set(99, vec![1, 2, 3]);
let bytes = attributes.to_bytes();
let parsed = Attributes::parse(&bytes).unwrap();
assert_eq!(parsed, attributes);
assert_eq!(parsed.font_size(), Some(6.0));
assert_eq!(parsed.bbox(), Some([1, 2, 30, 40]));
assert!(Attributes::parse(&bytes[..bytes.len() - 1]).is_err());
let array = join_array(&[bytes.clone(), vec![]]);
assert_eq!(split_array(&array).unwrap(), vec![bytes.as_slice(), &[][..]]);
}
#[test]
fn the_arrays_follow_the_single_block() {
use crate::database::{Row, SqlValue as Value};
let mut first = Attributes::default();
first.set_bbox([0, 0, 10, 10]);
let mut second = Attributes::default();
second.set_bbox([20, 0, 30, 10]);
let columns = ["TextLayerString", "TextLayerAttributes", "TextLayerStringArray", "TextLayerAttributesArray"];
let row = Row::new(
columns.iter().map(|c| c.to_string()).collect(),
vec![
Value::Blob(b"one".to_vec()),
Value::Blob(first.to_bytes()),
Value::Blob(join_array(&[b"two".to_vec()])),
Value::Blob(join_array(&[second.to_bytes()])),
],
);
let blocks = read_blocks(&row).unwrap();
assert_eq!(blocks.iter().map(|b| b.text.as_str()).collect::<Vec<_>>(), ["one", "two"]);
assert_eq!(blocks[1].attributes.bbox(), Some([20, 0, 30, 10]));
let single = Row::new(vec!["TextLayerString".into()], vec![Value::Blob(b"only".to_vec())]);
assert_eq!(read_blocks(&single).unwrap().len(), 1);
}
}