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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

steps:
- name: Install Harfbuzz
run: sudo apt-get install libharfbuzz0b libharfbuzz-dev
run: sudo apt-get install libharfbuzz0b libharfbuzz-dev libharfbuzz-subset0

- uses: actions/checkout@master

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ luacov.stats.out
.luacheckcache
api
*.src.rock
docs
4 changes: 2 additions & 2 deletions config.ld
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
project = 'luaharfbuzz'
description = 'Lua bindings to Harfbuzz'
full_description = 'The documentation is available on the @{https://github.com/harfbuzz/luaharfbuzz/wiki|wiki}'
full_description = 'The documentation is available at https://github.com/harfbuzz/luaharfbuzz/wiki'
use_markdown_titles = true
backtick_references = false
title = 'luaharfbuzz Documentation'
file = 'src/harfbuzz.luadoc'
file = {'src/harfbuzz.luadoc', 'src/harfbuzz-subset.luadoc'}
examples = 'examples'
format = 'markdown'
Binary file added fonts/AdventPro-VariableFont_wdth,wght.ttf
Binary file not shown.
45 changes: 28 additions & 17 deletions luaharfbuzz-scm-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ version = "scm-1"
source = {
url = "git://github.com/harfbuzz/luaharfbuzz"
}
dependencies = {
"lua >= 5.3"
}
description = {
summary = "Lua bindings for the Harfbuzz text shaping library",
homepage = "https://github.com/harfbuzz/luaharfbuzz",
Expand All @@ -12,33 +15,41 @@ description = {
issues_url = "https://github.com/harfbuzz/luaharfbuzz/issues",
labels = {"unicode", "fonts"}
}
dependencies = {
"lua >= 5.3"
}
build = {
type = "builtin",
modules = {
harfbuzz ="src/harfbuzz.lua",
luaharfbuzz= {
sources = {
"src/luaharfbuzz/luaharfbuzz.c",
"src/luaharfbuzz/blob.c",
"src/luaharfbuzz/face.c",
"src/luaharfbuzz/font.c",
"src/luaharfbuzz/buffer.c",
"src/luaharfbuzz/feature.c",
"src/luaharfbuzz/tag.c",
"src/luaharfbuzz/ot.c",
"src/luaharfbuzz/unicode.c",
"src/luaharfbuzz/script.c",
"src/luaharfbuzz/direction.c",
"src/luaharfbuzz/language.c",
"src/luaharfbuzz/variation.c",
"src/luaharfbuzz/class_utils.c"
"src/luaharfbuzz/luaharfbuzz.c",
"src/luaharfbuzz/blob.c",
"src/luaharfbuzz/face.c",
"src/luaharfbuzz/font.c",
"src/luaharfbuzz/buffer.c",
"src/luaharfbuzz/feature.c",
"src/luaharfbuzz/tag.c",
"src/luaharfbuzz/ot.c",
"src/luaharfbuzz/unicode.c",
"src/luaharfbuzz/set.c",
"src/luaharfbuzz/script.c",
"src/luaharfbuzz/direction.c",
"src/luaharfbuzz/language.c",
"src/luaharfbuzz/variation.c",
"src/class_utils.c",
},
libraries = {"harfbuzz"},
incdirs = {"$(HARFBUZZ_INCDIR)/harfbuzz"},
libdirs = {"$(HARFBUZZ_LIBDIR)"}
},
luaharfbuzzsubset = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we now have two separate libraries? I don’t mind either way, though I though we decided earlier to keep it as one library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the comment at #65 (comment) "The subset API should be in a separate Lua module, since not all users (especially LuaTeX) will need the subset API." in a way that two libraries should be created.

One would be easier to deploy of course. I wouldn't mind to merge both into one library.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m OK with it this way. We can change it later if, say, integration in TeX Live turned out to be harder. I took the opportunity to fix a few notpicks that I commented about. I have one more nitpick to fix and this should be ready.

sources = {
"src/luaharfbuzzsubset/luaharfbuzzsubset.c",
"src/luaharfbuzzsubset/subset-input.c",
"src/class_utils.c",
},
libraries = {"harfbuzz-subset"},
incdirs = {"$(HARFBUZZ_INCDIR)/harfbuzz"},
libdirs = {"$(HARFBUZZ_LIBDIR)"}
}
}
}
Expand Down
143 changes: 143 additions & 0 deletions spec/subset_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package.path = "./src/?.lua;./src/?/init.lua;" .. package.path

local harfbuzzsubset = require("harfbuzzsubset")
local harfbuzz = require("harfbuzz")

describe("harfbuzz module", function()
it("can be required when subset module is also required", function()
assert.is_not_nil(harfbuzz)
assert.is_not_nil(harfbuzzsubset)
end)
end)


describe("harfbuzzsubset basic usage", function()
local hb, hb_subset
local font_path = "fonts/AdventPro-VariableFont_wdth,wght.ttf"

setup(function()
hb = require "harfbuzz"
hb_subset = require "harfbuzzsubset"
end)

it("returns a valid version string", function()
assert.are_equal("string", type(hb_subset.version()))
end)

it("creates a subset input object", function()
local input = hb_subset.subset_input.new()
assert.truthy(input)
-- minimal test of methods existing
assert.is_function(input.keep_everything)
assert.is_function(input.pin_axis_location)
end)

it("can subset a variable font without errors", function()
local face = hb.Face.new(font_path)
local wght = hb.Tag.new("wght")
local wdth = hb.Tag.new("wdth")

local input = hb_subset.subset_input.new()
input:pin_axis_location(face, wght, 100)
input:pin_axis_location(face, wdth, 100)
input:keep_everything()

local ok, new_face = pcall(function()
return hb_subset.subset(face, input)
end)

assert.is_true(ok)
assert.truthy(new_face)
assert.are_not.equal(face, new_face)
end)

it("produces a non-empty blob after subsetting", function()
local face = hb.Face.new(font_path)
local wght = hb.Tag.new("wght")
local wdth = hb.Tag.new("wdth")

local input = hb_subset.subset_input.new()
input:pin_axis_location(face, wght, 100)
input:pin_axis_location(face, wdth, 100)
input:keep_everything()

local new_face = hb_subset.subset(face, input)

local blob = new_face:blob()
assert.truthy(blob)
assert.is_function(blob.get_data)

local data = blob:get_data()
assert.is_string(data)
assert.is_true(#data > 0)
end)

it("preserves glyph count when keep_everything is used", function()
local face = hb.Face.new(font_path)
local orig_glyphs = face:get_glyph_count()

local wght = hb.Tag.new("wght")
local wdth = hb.Tag.new("wdth")
local input = hb_subset.subset_input.new()
input:pin_axis_location(face, wght, 100)
input:pin_axis_location(face, wdth, 100)
input:keep_everything()

local new_face = hb_subset.subset(face, input)
local subset_glyphs = new_face:get_glyph_count()

assert.equals(orig_glyphs, subset_glyphs)
end)

it("produces different blobs for different axis locations", function()
local face = hb.Face.new(font_path)
local wght = hb.Tag.new("wght")
local wdth = hb.Tag.new("wdth")

local input1 = hb_subset.subset_input.new()
input1:pin_axis_location(face, wght, 100)
input1:pin_axis_location(face, wdth, 100)
input1:keep_everything()
local face_light = hb_subset.subset(face, input1)
local blob_light = face_light:blob():get_data()

local input2 = hb_subset.subset_input.new()
input2:pin_axis_location(face, wght, 900)
input2:pin_axis_location(face, wdth, 100)
input2:keep_everything()
local face_bold = hb_subset.subset(face, input2)
local blob_bold = face_bold:blob():get_data()

assert.is_true(
blob_light ~= blob_bold,
("expected different blobs for axis locations, but they are identical (len=%d bytes)")
:format(#blob_light)
)
assert.is_true(#blob_light > 0, "blob_light is empty")
assert.is_true(#blob_bold > 0, "blob_bold is empty")
end)

it("reduces glyph count when subsetting to a single Unicode", function()
local face = hb.Face.new(font_path)
local orig_glyphs = face:get_glyph_count()

local input = hb_subset.subset_input.new()

local uset = input:unicode_set()
uset:add(0x41) -- oder string.byte("A")

local new_face = hb_subset.subset(face, input)
local subset_glyphs = new_face:get_glyph_count()

-- Sanity-Checks mit vernünftiger Fehlermeldung, ohne Binärmüll:
assert.is_true(
subset_glyphs > 0,
("expected subset glyph count > 0, got %d"):format(subset_glyphs)
)
assert.is_true(
subset_glyphs < orig_glyphs,
("expected fewer glyphs after subsetting, got %d (orig %d)")
:format(subset_glyphs, orig_glyphs)
)
end)
end)
2 changes: 1 addition & 1 deletion src/luaharfbuzz/class_utils.c → src/class_utils.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Utility functions to create Lua classes.
#include "luaharfbuzz.h"
#include "common.h"

int register_class(lua_State *L, const char *name, const luaL_Reg *methods, const luaL_Reg *functions, const luahb_constant_t *constants) {
luaL_newmetatable(L, name);
Expand Down
30 changes: 30 additions & 0 deletions src/common.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <hb.h>
#include <hb-ot.h>
#include <string.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

typedef hb_blob_t* Blob;
typedef hb_face_t* Face;
typedef hb_font_t* Font;
typedef hb_buffer_t* Buffer;
typedef hb_set_t* Set;
typedef hb_feature_t Feature;
typedef hb_tag_t Tag;
typedef hb_script_t Script;
typedef hb_direction_t Direction;
typedef hb_language_t Language;
typedef hb_variation_t Variation;

typedef struct luahb_constant_t {
const char *name;
int value;
} luahb_constant_t;

// Functions to create classes and push them onto the stack
int register_class(lua_State *L, const char *name, const luaL_Reg * methods, const luaL_Reg *functions, const luahb_constant_t* constants);
111 changes: 111 additions & 0 deletions src/harfbuzz-subset.luadoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
-----------
-- Lua bindings to the Harfbuzz subset API.
-- * [Wiki](http://github.com/harfbuzz/luaharfbuzz/wiki)
-- * [Source on Github](https://github.com/harfbuzz/luaharfbuzz)
-- * [API Coverage Status](https://github.com/harfbuzz/luaharfbuzz/blob/master/status/done.txt)
--
-- This module provides bindings to the HarfBuzz `hb-subset` API. It is
-- designed to be used together with the main @{harfbuzz} module.
--
-- Typical usage:
--
-- local hb = require('harfbuzz')
-- local hb_subset = require("harfbuzzsubset")
--
-- local input = hb_subset.subset.create()
--
-- local wght = hb.Tag.new("wght")
-- local wdth = hb.Tag.new("wdth")
-- local face = hb.Face.new("myfont.ttf")
--
-- input:pin_axis_location(face,wght, 100)
-- input:pin_axis_location(face,wdth, 100)
--
-- input:keep_everything()
-- new_face = hb_subset.subset(face, input)
--
-- local blob = new_face:blob()
-- local fh, err = io.open("out.ttf", "wb")
-- if not fh then error(err) end
-- fh:write(blob:get_data())
-- fh:close()
--
-- The module returned by `require "harfbuzzsubset"` has two fields:
--
-- * `subset_input` — helpers and methods related to @{SubsetInput}
-- * `subset` — helpers and methods related to @{SubsetInput}
--
-- @author Patrick Gundlach <<gundlach@speedata.de>>
-- @copyright 2025
-- @license MIT
-- @module harfbuzzsubset


--- Wraps `hb_subset_or_fail`.
--
-- Performs the actual subset operation, producing a new `Face` that only
-- contains the glyphs and data configured on this input.
--
-- @param face source `Face` object to subset.
-- @param input `SubsetInput` object to subset based on.
-- @return new `Face` object representing the subset, or `nil` on failure.
-- @function subset


--- Wraps `hb_version`.
-- @function version

--- Lua wrapper for `hb_subset_input_t` type.
--
-- Objects of this type control which parts of a `Face` are kept when
-- creating a subset.
--
-- Instances are usually created via @{subset_input.new}.
--
-- @type SubsetInput


--- Wraps `hb_subset_input_create_or_fail`.
--
-- Initializes a new `hb_subset_input_t`. The returned object can be used to
-- configure which glyphs and features are kept when subsetting a face.
--
-- @return `SubsetInput` object, or `nil` on failure.
-- @function subset_input.new


--- Wraps `hb_subset_input_unicode_set`.
--
-- Returns the Unicode set associated with this subset input. The returned
-- @{harfbuzz.Set} can be modified (e.g. via @{harfbuzz.Set:add}) to control which
-- Unicode codepoints are included in the subset.
--
-- @return `Set` object representing the Unicode set, or `nil` on failure.
-- @function SubsetInput:unicode_set


--- Wraps `hb_subset_input_pin_axis_location`.
--
-- Pins a variation axis at a specific value for subsetting.
--
-- @param face `Face` object whose design space is being subset.
-- @param tag `Tag` object representing the axis tag (e.g. `"wght"`, `"wdth"`).
-- @param value numeric axis value to pin.
-- @return `true` on success, `false` on failure.
-- @function SubsetInput:pin_axis_location


--- Wraps `hb_subset_input_keep_everything`.
--
-- Configures the input so that everything in the original face is kept.
-- This is useful as a starting point before applying more specific filters.
--
-- @function SubsetInput:keep_everything


--- Wraps `hb_subset_input_destroy`.
--
-- Destructor for subset inputs. Normally this is invoked automatically by
-- the garbage collector; applications rarely need to call it directly.
--
-- @function SubsetInput:__gc
Loading
Loading