Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Can't define a variable scalar in a non-variable font" #1140

Open
yanone opened this issue Jan 11, 2025 · 8 comments
Open

"Can't define a variable scalar in a non-variable font" #1140

yanone opened this issue Jan 11, 2025 · 8 comments

Comments

@yanone
Copy link
Contributor

yanone commented Jan 11, 2025

My build fails with that error message, trying to generate static instances.

Is this a variable scalar?:
pos a' b <0 (wght=100:0 wght=400:50 wght=1000:100) 0 0>;

If so, I feel like this should get resolved for generating statics in the same way that outline interpolation gets resolved, which should ultimately be the same calculation.

Or am I getting something wrong?

Is it the norm today to cut statics via varLib.instancer and I just ignore making statics the old way?

@simoncozens
Copy link
Contributor

That is a variable scalar.

We don't have variable fea instancing built in to fontmake yet (I have a varfea instancer somewhere) so fontmake doesn't know how to turn that into the appropriate feature code when building a static font. You can use Glyphs number values instead of variable feature syntax, and it'll work fine.

Latta AI seeks to solve problems in open source projects as part of its mission to support developers around the world. Learn more about our mission at latta.ai/ourmission .

Oh FFS.

@yanone
Copy link
Contributor Author

yanone commented Jan 11, 2025

You can use Glyphs number values instead of variable feature syntax, and it'll work fine.

I have about 4,000 tashkeel positioning rules. I already compressed them into groups as much as possible. I don't think it's a good idea to do it via Glyphs.

Other than that, where do they get resolved? Because I definitely want to generate statics outside of Glyphs as well, do they not also get resolved in fontmake then? Where's the difference?

@simoncozens
Copy link
Contributor

simoncozens commented Jan 11, 2025

Other than that, where do they get resolved? Because I definitely want to generate statics outside of Glyphs as well, do they not also get resolved in fontmake then? Where's the difference?

I'm not sure what you mean by "resolved" here.

Let's back up - did you write that variable feature rule yourself, or was it generated by fontmake? I'm assuming you must have written it yourself, because nothing automatically generates contextual positioning rules. (Glyphs can generate contextual anchoring rules, but that's different.) But then you ask if it would work outside of Glyphs, and I'm surprised it works inside of Glyphs because Glyphs doesn't support that variable feature syntax.

I'll try and explain what's going on and we can hopefully work out where the problem is.

So you have some positioning rules which have "variable scalars" in them - numbers which change across the designspace. The way these get handled is that the feature code compiler turns them into (a) the default number, which lives in the GPOS table, and (b) a set of deltas which explain how they vary, and these live in the item variation store in the GDEF table.

When you compile a variable font this all works fine. But you want to make some statics. The feature compiler takes the same feature code, and tries to compile it, but when it hits the variable scalar it has a problem because you no longer want the default number to live in the GPOS table, you want the location-specific interpolated number. So ideally you want the "feature compiler" part of fontmake to rewrite that rule from

pos a' b <0 (wght=100:0 wght=400:50 wght=1000:100) 0 0>;

to a "normal" positioning rule (for example, when compiling the bold weight)

pos a' b <0 100 0 0>;

The problem is that (a) it doesn't currently do that when it generates statics, and (b) it can't easily do that, because the "feature compiler" part of fontmake doesn't store information about what location is being compiled, because variable feature syntax is a new thing and it's just not designed to do this, so it can't perform the interpolation.

Cutting statics from the VF would work (subject to the usual differences), or a custom build chain which interpolates the variable FEA into plain fea. I have one somewhere.

@yanone
Copy link
Contributor Author

yanone commented Jan 11, 2025

By resolved I mean a variable scalar turned into a location-specific interpolated number.

At this point I'm just trying to understand the difference between fontmake variable scalars and Glyphs number values. Since they are just saved into the Glyphs file as-is, there is no technical difference to fontmake-flavour variable scalars other than storage location. Since you said above that that approach would work to generate statics, where in the build chain do those get interpolated then?

Is that not subject to the same limitations that fontmake suffers from for variable scalars? Or, asked differently, if it works using Glyphs values, can that not be used for variable scalars in the same way?

Otherwise I'm fine with cutting instances using the varLib.instancer and brushing up those few values that don't get changed is no problem, given that the publisher accepts TTF statics only. I need to ask, because I think so far we've been talking about TTFs and OTFs.

If OTFs are required, I'm gonna ask you to dig up your custom build chain. Mine is already so customized that it doesn't make a difference to me.
Otherwise I'll use the instancer.

@simoncozens
Copy link
Contributor

At this point I'm just trying to understand the difference between fontmake variable scalars and Glyphs number values. Since they are just saved into the Glyphs file as-is, there is no technical difference to fontmake-flavour variable scalars other than storage location. Since you said above that that approach would work to generate statics, where in the build chain do those get interpolated then?

Oh, I see. When Glyphs files are turned into UFOs, each master gets its own feature file, and any rules with number variables are rewritten with the variables filled in. (This means that the master-specific feature files end up being different, which causes fontmake do an old-style "binary merge" when building a variable font, instead of computing the GDEF table IVS directly.) I don't know if that works for interpolated, non-master instances.

@yanone
Copy link
Contributor Author

yanone commented Jan 11, 2025

Okay, so, I'm already doing a two-step build where I'm writing UFOs from the Glyphs file first because it helps with debugging, then generate from there, and those masters already each have their own feature file.

What prevents me from writing a small tool that will just pick and replace the corresponding master value in each scalar? Is that not the same then as you were describing with the Glyphs-to-UFO conversion?

By the time the master UFOs get written, I don't see why the scalars should even still exist. What am I missing?

@yanone
Copy link
Contributor Author

yanone commented Jan 12, 2025

So, I tried. Running the below code on the designspace file to replace the scalars allows the generation of statics by fontmake, but while it all makes sense in my head, the values are off and I'm under too much pressure to investigate right now.

(I did check that the replaced values in the feature files make sense.)

See difference of stacked damma between VF (correct) and static (wrong):
Image

Furthermore, while I thought the replaced scalars would also allow generating a VF, another mistake is introduced somewhere, but I don't know how to debug it:

fontmake: Error: In 'master_ufo/Sukoon.designspace': Generating fonts from Designspace failed: 

Couldn't merge the fonts, because some values were different, but should have
been the same. This happened while performing the following operation:
GPOS.table.FeatureList.FeatureRecord[0].Feature.LookupListIndex[0]

The problem is likely to be in Sukoon Hairline:
Expected to see [0]==363, instead saw 376

So I'm replacing the scalars only after generating the VF, before the statics, which works but throws the values off.

Since I can't deliver faulty values, I'm currently still bound to instantiating statics with varLib.instancer

import sys
import os
import re
from fontTools.designspaceLib import *

designspace = DesignSpaceDocument()
designspace.read(sys.argv[-1])


def get_axis(name):
    for a in designspace.axes:
        if a.name == name:
            return a


# Function to read the features.fea file of a source
def read_features_file(source_path):
    features_path = os.path.join(source_path, "features.fea")
    if os.path.exists(features_path):
        with open(features_path, "r", encoding="utf-8") as f:
            return f.read()
    else:
        return None


def save_features_file(source_path, features_content):
    features_path = os.path.join(source_path, "features.fea")
    with open(features_path, "w", encoding="utf-8") as f:
        f.write(features_content)


pattern = r"\(.*?\)"

for source in designspace.sources:
    # is real master, not intermediate master
    if not source.layerName:

        # Assemble the scalar descriptor
        master_descriptor = []
        for axis_name, axis_value in source.location.items():
            axis = get_axis(axis_name)
            if axis.map:
                for user_value, designspave_value in axis.map:
                    if axis_value == designspave_value:
                        break

                master_descriptor.append(f"{axis.tag}={int(user_value)}")
            else:
                master_descriptor.append(f"{axis.tag}={int(axis_value)}")

        master_descriptor = ",".join(master_descriptor) # equals "wght=50,KSHD=0,SWSH=0" etc.

        def replacement_function(match):
            matched_text = match.group(0)[1:-1] # remove brackets
            parts = matched_text.split(" ")
            for part in parts:
                this_descriptor, value = part.split(":")
                if this_descriptor == master_descriptor:
                    return value
            raise ValueError(f"Could not find scalar for {master_descriptor}")

        features_content = read_features_file(source.path)
        if features_content:
            output_string = re.sub(pattern, replacement_function, features_content)
            save_features_file(source.path, output_string)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants
@simoncozens @yanone and others