Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions python/pyharfrust/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Buffer,
Direction,
Feature,
Font,
GlyphBuffer,
GlyphInfo,
GlyphPosition,
Language,
Script,
Variation,
Expand All @@ -15,6 +19,10 @@
"Buffer",
"Direction",
"Feature",
"Font",
"GlyphBuffer",
"GlyphInfo",
"GlyphPosition",
"Language",
"Script",
"Variation",
Expand Down
2 changes: 1 addition & 1 deletion src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ impl PyBuffer {
PyValueError::new_err("Buffer has been consumed by shape()")
}

fn as_ref_buf(&self) -> PyResult<&UnicodeBuffer> {
pub(crate) fn as_ref_buf(&self) -> PyResult<&UnicodeBuffer> {
self.inner.as_ref().ok_or_else(Self::consumed_err)
}

Expand Down
178 changes: 178 additions & 0 deletions src/font.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use std::str::FromStr;

use harfrust::{Feature, FontRef, Shaper, ShaperData, ShaperInstance, Variation};
use pyo3::exceptions::{PyRuntimeError, PyTypeError, PyValueError};
use pyo3::prelude::*;

use crate::buffer::PyBuffer;
use crate::glyph::PyGlyphBuffer;
use crate::types::{PyFeature, PyVariation};

#[pyclass(name = "Font", unsendable)]
pub struct PyFont {
data: Vec<u8>,
face_index: u32,
shaper_data: ShaperData,
instance: Option<ShaperInstance>,
point_size: Option<f32>,
}

impl PyFont {
fn build_from_bytes(data: Vec<u8>, face_index: u32) -> PyResult<Self> {
// Validate the font and build ShaperData up front so subsequent calls
// don't need to re-parse. ShaperData is 'static — it borrows FontRef
// only during construction.
let shaper_data = {
let font = FontRef::from_index(&data, face_index)
.map_err(|e| PyRuntimeError::new_err(format!("failed to parse font: {e}")))?;
ShaperData::new(&font)
};
Ok(Self {
data,
face_index,
shaper_data,
instance: None,
point_size: None,
})
}

pub(crate) fn font_ref(&self) -> PyResult<FontRef<'_>> {
FontRef::from_index(&self.data, self.face_index)
.map_err(|e| PyRuntimeError::new_err(format!("failed to parse font: {e}")))
}

pub(crate) fn build_shaper<'a>(&'a self, font: &FontRef<'a>) -> Shaper<'a> {
self.shaper_data
.shaper(font)
.instance(self.instance.as_ref())
.point_size(self.point_size)
.build()
}
}

#[pymethods]
impl PyFont {
#[new]
#[pyo3(signature = (path, face_index=0))]
fn new(path: &str, face_index: u32) -> PyResult<Self> {
let data = std::fs::read(path)
.map_err(|e| PyRuntimeError::new_err(format!("failed to read {path:?}: {e}")))?;
Self::build_from_bytes(data, face_index)
}

#[staticmethod]
#[pyo3(signature = (data, face_index=0))]
fn from_bytes(data: Vec<u8>, face_index: u32) -> PyResult<Self> {
Self::build_from_bytes(data, face_index)
}

#[getter]
fn face_index(&self) -> u32 {
self.face_index
}

#[getter]
fn units_per_em(&self) -> PyResult<i32> {
let font = self.font_ref()?;
Ok(self.build_shaper(&font).units_per_em())
}

fn set_variations(&mut self, variations: &Bound<'_, PyAny>) -> PyResult<()> {
let vars = parse_variations(variations)?;
if vars.is_empty() {
self.instance = None;
return Ok(());
}
// Direct field access (rather than self.font_ref()) so the borrow
// checker can split self.data (immutable) from self.instance (mutable).
let font = FontRef::from_index(&self.data, self.face_index)
.map_err(|e| PyRuntimeError::new_err(format!("failed to parse font: {e}")))?;
match &mut self.instance {
Some(inst) => inst.set_variations(&font, vars),
None => {
self.instance = Some(ShaperInstance::from_variations(&font, vars));
}
}
Ok(())
}

#[pyo3(signature = (size))]
fn set_point_size(&mut self, size: Option<f32>) {
self.point_size = size;
}

#[pyo3(signature = (buffer, features=None))]
fn shape(
&self,
buffer: &mut PyBuffer,
features: Option<&Bound<'_, PyAny>>,
) -> PyResult<PyGlyphBuffer> {
let feats = match features {
Some(any) => parse_features(any)?,
None => Vec::new(),
};
// Reject Invalid direction up front — harfrust panics on it otherwise,
// surfacing as PanicException in Python instead of a clean error.
if buffer.as_ref_buf()?.direction() == harfrust::Direction::Invalid {
return Err(PyValueError::new_err(
"buffer direction is unset; call buffer.guess_segment_properties() \
or assign buffer.direction before shaping",
));
}
let inner = buffer.take_inner()?;
let font = self.font_ref()?;
let shaper = self.build_shaper(&font);
Ok(PyGlyphBuffer::wrap(shaper.shape(inner, &feats)))
}

fn __repr__(&self) -> String {
format!(
"Font(face_index={}, bytes={})",
self.face_index,
self.data.len()
)
}
}

fn parse_features(any: &Bound<'_, PyAny>) -> PyResult<Vec<Feature>> {
if let Ok(s) = any.extract::<&str>() {
return parse_csv(s, "feature", Feature::from_str);
}
if let Ok(seq) = any.extract::<Vec<PyFeature>>() {
return Ok(seq.into_iter().map(|f| f.0).collect());
}
Err(PyTypeError::new_err(
"expected a sequence of Feature objects or a string",
))
}

fn parse_variations(any: &Bound<'_, PyAny>) -> PyResult<Vec<Variation>> {
if let Ok(s) = any.extract::<&str>() {
return parse_csv(s, "variation", Variation::from_str);
}
if let Ok(seq) = any.extract::<Vec<PyVariation>>() {
return Ok(seq.into_iter().map(|v| v.0).collect());
}
Err(PyTypeError::new_err(
"expected a sequence of Variation objects or a string",
))
}

fn parse_csv<T, E>(s: &str, label: &str, parse: impl Fn(&str) -> Result<T, E>) -> PyResult<Vec<T>> {
let mut out = Vec::new();
for piece in s.split(',') {
let piece = piece.trim();
if piece.is_empty() {
continue;
}
let value = parse(piece)
.map_err(|_| PyValueError::new_err(format!("invalid {label}: {piece:?}")))?;
out.push(value);
}
Ok(out)
}

pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyFont>()?;
Ok(())
}
180 changes: 180 additions & 0 deletions src/glyph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use harfrust::{GlyphBuffer, GlyphInfo, GlyphPosition, SerializeFlags};
use pyo3::exceptions::{PyIndexError, PyValueError};
use pyo3::prelude::*;

use crate::buffer::PyBuffer;
use crate::font::PyFont;

// ---------------------------------------------------------------------------
// GlyphInfo
// ---------------------------------------------------------------------------

#[pyclass(name = "GlyphInfo", frozen, from_py_object)]
#[derive(Clone, Copy)]
pub struct PyGlyphInfo {
#[pyo3(get)]
glyph_id: u32,
#[pyo3(get)]
cluster: u32,
#[pyo3(get)]
unsafe_to_break: bool,
#[pyo3(get)]
unsafe_to_concat: bool,
#[pyo3(get)]
safe_to_insert_tatweel: bool,
}

impl PyGlyphInfo {
fn from_info(info: &GlyphInfo) -> Self {
Self {
glyph_id: info.glyph_id,
cluster: info.cluster,
unsafe_to_break: info.unsafe_to_break(),
unsafe_to_concat: info.unsafe_to_concat(),
safe_to_insert_tatweel: info.safe_to_insert_tatweel(),
}
}
}

#[pymethods]
impl PyGlyphInfo {
fn __repr__(&self) -> String {
format!(
"GlyphInfo(glyph_id={}, cluster={})",
self.glyph_id, self.cluster
)
}
}

// ---------------------------------------------------------------------------
// GlyphPosition
// ---------------------------------------------------------------------------

#[pyclass(name = "GlyphPosition", frozen, from_py_object)]
#[derive(Clone, Copy)]
pub struct PyGlyphPosition {
#[pyo3(get)]
x_advance: i32,
#[pyo3(get)]
y_advance: i32,
#[pyo3(get)]
x_offset: i32,
#[pyo3(get)]
y_offset: i32,
}

impl PyGlyphPosition {
fn from_pos(pos: &GlyphPosition) -> Self {
Self {
x_advance: pos.x_advance,
y_advance: pos.y_advance,
x_offset: pos.x_offset,
y_offset: pos.y_offset,
}
}
}

#[pymethods]
impl PyGlyphPosition {
fn __repr__(&self) -> String {
format!(
"GlyphPosition(x_advance={}, y_advance={}, x_offset={}, y_offset={})",
self.x_advance, self.y_advance, self.x_offset, self.y_offset
)
}
}

// ---------------------------------------------------------------------------
// GlyphBuffer
// ---------------------------------------------------------------------------

#[pyclass(name = "GlyphBuffer", unsendable)]
pub struct PyGlyphBuffer {
inner: Option<GlyphBuffer>,
}

impl PyGlyphBuffer {
pub(crate) fn wrap(buf: GlyphBuffer) -> Self {
Self { inner: Some(buf) }
}

fn consumed_err() -> PyErr {
PyValueError::new_err("GlyphBuffer has been consumed by clear()")
}

fn as_ref_buf(&self) -> PyResult<&GlyphBuffer> {
self.inner.as_ref().ok_or_else(Self::consumed_err)
}
}

#[pymethods]
impl PyGlyphBuffer {
fn __len__(&self) -> PyResult<usize> {
Ok(self.as_ref_buf()?.len())
}

#[getter]
fn glyph_infos(&self) -> PyResult<Vec<PyGlyphInfo>> {
Ok(self
.as_ref_buf()?
.glyph_infos()
.iter()
.map(PyGlyphInfo::from_info)
.collect())
}

#[getter]
fn glyph_positions(&self) -> PyResult<Vec<PyGlyphPosition>> {
Ok(self
.as_ref_buf()?
.glyph_positions()
.iter()
.map(PyGlyphPosition::from_pos)
.collect())
}

// Implementing __getitem__ + __len__ makes the buffer iterable in Python
// via the legacy iteration protocol, so `for info, pos in gbuf:` works
// without a separate iterator class.
fn __getitem__(&self, index: isize) -> PyResult<(PyGlyphInfo, PyGlyphPosition)> {
let buf = self.as_ref_buf()?;
let len = buf.len() as isize;
let idx = if index < 0 { index + len } else { index };
if idx < 0 || idx >= len {
return Err(PyIndexError::new_err("glyph index out of range"));
}
let i = idx as usize;
Ok((
PyGlyphInfo::from_info(&buf.glyph_infos()[i]),
PyGlyphPosition::from_pos(&buf.glyph_positions()[i]),
))
}

fn clear(&mut self) -> PyResult<PyBuffer> {
let inner = self.inner.take().ok_or_else(Self::consumed_err)?;
Ok(PyBuffer {
inner: Some(inner.clear()),
})
}

fn serialize(&self, font: PyRef<'_, PyFont>) -> PyResult<String> {
let buf = self.as_ref_buf()?;
let font_ref = font.font_ref()?;
let shaper = font.build_shaper(&font_ref);
Ok(buf.serialize(&shaper, SerializeFlags::default()))
}

fn __repr__(&self) -> String {
match self.inner.as_ref() {
Some(b) => format!("GlyphBuffer(len={})", b.len()),
None => "GlyphBuffer(<consumed>)".to_string(),
}
}
}

pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyGlyphInfo>()?;
m.add_class::<PyGlyphPosition>()?;
m.add_class::<PyGlyphBuffer>()?;
Ok(())
}
Loading
Loading