Hi Hans,

The internal "pack_result_tagged" function that you added for potraceing
seems to be useful beyond bitmapped fonts, because it lets you
manipulate arbitrary CFF outlines from Lua. As far as I'm aware, this
makes ConTeXt the only program that can modify and create non-Type 3
fonts while generating a document.

This seems to be quite useful for a better fake bold. The traditional
PDF method of applying a thin stroke to characters can have some
rendering issues and doesn't give you very much control, but with
"pack_result_tagged", we can make a "real" fake bold font where the font
itself has been emboldened (versus just applying a PDF effect). This
also gives us unlimited control, so it should theoretically be possible
to replicate FontForge's very good bold/italicizing algorithms to get
much better results than you can get from PDF effects.

I've attached a very hacky demonstration that needs to be compiled with
MkIV (despite using LMTX-only features). The offsetting algorithm isn't
quite correct around straight lines, but it's still a fairly neat demo.

Thanks,
-- Max

#!/usr/bin/env -S context --luatex --forcecld
local insert = table.insert
local append = table.append

do
    local data = io.loaddata(resolvers.findfile("font-cff.lmt"))
    data = data:gsub("pack_result_tagged =", "fonts.handlers.otf.pack_result_tagged ="):gsub("<const>", "")
    load(data)()
end

local font_id, _ = fonts.definers.define {
    -- The path to the font file
    lookup = "file",
    name = "texgyrepagella-regular.otf",

    -- The size of the font
    size = tex.sp("12pt"),

    -- Use the default features
    method = "featureset",
    detail = "default",
}
local tfmdata = font.getcopy(font_id)
tfmdata.fullname = nil
tfmdata.psname = nil
tfmdata.streamprovider = 1
tfmdata.name = "fakebold"

local shapes = fonts.hashes.shapes[font_id].glyphs
local streams = fonts.hashes.streams[font_id].streams

local function subdivide_segments(inputs)
    local outputs = {}
    local x0, y0 = 0, 0
    for i, input in ipairs(inputs) do
        local command = input[#input]
        if command == "l" then
            --[[ -- Convert line to cubic bezier
            local x1, y1 = input[1], input[2]
            local slope_x, slope_y = (x1 - x0) / 3, (y1 - y0) / 3
            outputs[#outputs + 1] = {
                x0 + slope_x, y0 + slope_y,
                x1 - slope_x, y1 - slope_y,
                x1, y1,
                "c"
            } --]]
            -- Leave lines as-is
            outputs[#outputs + 1] = input
        elseif command == "c" then
            -- Divide the cubic bezier into two cubic beziers
            local x1, y1 = input[1], input[2]
            local x2, y2 = input[3], input[4]
            local x3, y3 = input[5], input[6]

            local x01, y01 = (x0 + x1) / 2, (y0 + y1) / 2
            local x12, y12 = (x1 + x2) / 2, (y1 + y2) / 2
            local x23, y23 = (x2 + x3) / 2, (y2 + y3) / 2
            local x012, y012 = (x01 + x12) / 2, (y01 + y12) / 2
            local x123, y123 = (x12 + x23) / 2, (y12 + y23) / 2
            local x0123, y0123 = (x012 + x123) / 2, (y012 + y123) / 2

            local xa1, ya1 = x01, y01
            local xa2, ya2 = x012, y012
            local xa3, ya3 = x0123, y0123

            local xb1, yb1 = x123, y123
            local xb2, yb2 = x23, y23
            local xb3, yb3 = x3, y3

            outputs[#outputs + 1] = { xa1, ya1, xa2, ya2, xa3, ya3, "c" }
            outputs[#outputs + 1] = { xb1, yb1, xb2, yb2, xb3, yb3, "c" }
        elseif command == "m" or command == "move" then
            outputs[#outputs + 1] = input
        else
            error("Unknown segment command: " .. tostring(command))
        end

        x0, y0 = input[#input - 2], input[#input - 1]
    end

    return outputs
end

local function get_xy(segments, segment_index, point_index, offset)
    local segment = segments[segment_index]

    point_index = point_index + 2 * offset

    local x, y
    while true do
        x, y = segment[point_index], segment[point_index + 1]

        if x and y then
            break
        end

        if point_index > #segment - 1 then
            segment_index = segment_index + 1
            local prev_length = #segment
            segment = segments[segment_index]
            if not segment then
                segment = segments[1]
                x, y = segment[1], segment[2]
                return x, y
            end
            point_index = point_index - prev_length + 1
        end

        if point_index <= 0 then
            segment_index = segment_index - 1
            segment = segments[segment_index]
            if not segment then
                segment = segments[#segments]
                x, y = segment[#segment - 2], segment[#segment - 1]
                return x, y
            end
            point_index = #segment - 1 + point_index
        end
    end

    return x, y
end

local offset_distance = -17.5

local function process(character)
    local sections = { {} }

    for _, segment in ipairs(character.segments) do
        local type = segment[#segment]
        if type == "move" then
            insert(sections, {})
        else
            insert(sections[#sections], segment)
        end
    end

    for i, section in ipairs(sections) do
        local section = subdivide_segments(section)
        local new = table.copy(section)
        for j, segment in ipairs(section) do
            for k = 1, #segment - 2, 2 do
                local x_prev, y_prev = get_xy(section, j, k, -1)
                local x_next, y_next = get_xy(section, j, k, 1)
                local x, y = segment[k], segment[k + 1]

                local dx, dy = x_next - x_prev, y_next - y_prev
                local length = math.sqrt(dx^2 + dy^2)
                local normal_x, normal_y = -dy / length, dx / length

                if normal_x ~= normal_x then normal_x = 0 end
                if normal_y ~= normal_y then normal_y = 0 end

                x = x + (normal_x * offset_distance)
                y = y + (normal_y * offset_distance)

                new[j][k] = x
                new[j][k + 1] = y
            end
        end
        sections[i] = new
    end

    character.segments = {}
    for _, section in ipairs(sections) do
        append(character.segments, section)
        insert(character.segments, { "move", "close" })
    end

    character.tounicode = ("%X"):format(character.unicode or 0)
    local stream = fonts.handlers.otf.pack_result_tagged(
        character.segments,
        character.width,
        0,
        0
    )
    return stream
end

for index, character in pairs(shapes) do
    streams[index] = process(character)
end

local font_id = font.define(tfmdata)
fonts.hashes.streams[font_id] = { streams = streams }

token.set_macro("newfakebold", ([[\setfontid%d]]):format(font_id))

context[[
    \setupbodyfont[pagella, 12pt]

    \define\SampleText{The quick brown fox jumps over the lazy dog.}

    \definefontfeature[oldfakebold][effect={width=0.35}]
    \definefont[oldfakebold][file:texgyrepagella-regular.otf*default,oldfakebold @ 12pt]

    \startTEXpage[offset=1ex]
        \setupTABLE[column][1][align=flushright, style=tt]
        \setupTABLE[column][2][align=flushleft, loffset=1em]
        \startTABLE[frame=off]
            \NC Regular \NC \SampleText \NC\NR
            \NC Real Bold \NC \bf \SampleText \NC\NR
            \NC Old Fake Bold \NC \oldfakebold \SampleText \NC\NR
            \NC New Fake Bold \NC \newfakebold \SampleText \NC\NR
        \stopTABLE
    \stopTEXpage
]]
___________________________________________________________________________________
If your question is of interest to others as well, please add an entry to the 
Wiki!

maillist : [email protected] / 
https://mailman.ntg.nl/mailman3/lists/ntg-context.ntg.nl
webpage  : https://www.pragma-ade.nl / https://context.aanhet.net (mirror)
archive  : https://github.com/contextgarden/context
wiki     : https://wiki.contextgarden.net
___________________________________________________________________________________

Reply via email to