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
25 changes: 22 additions & 3 deletions CodenameOne/src/com/codename1/ui/Font.java
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,24 @@ public static Font createTrueTypeFont(String fontName, float size, byte sizeUnit
derive(Display.getInstance().convertToPixels(size, sizeUnit), STYLE_PLAIN);
}

/// True when the file name carries an extension the bundled font loaders accept.
///
/// Both TrueType and OpenType are supported: every port loads fonts through an
/// API that reads the SFNT container regardless of whether the outlines are
/// glyf or CFF (Core Text on iOS, Typeface on Android, DirectWrite on Windows,
/// FreeType on Linux, java.awt on the simulator and FontFace in the browser).
/// The comparison is case insensitive so a file named `.TTF` isn't rejected
/// for its capitalisation alone. It goes through regionMatches rather than
/// toLowerCase so the answer can't depend on the default locale.
static boolean isSupportedFontFile(String fileName) {
return endsWithIgnoreCase(fileName, ".ttf") || endsWithIgnoreCase(fileName, ".otf");
}

private static boolean endsWithIgnoreCase(String value, String suffix) {
return value != null && value.length() >= suffix.length()
&& value.regionMatches(true, value.length() - suffix.length(), suffix, 0, suffix.length());
}

/// Creates a true type font with the given name/filename (font name might be different from the file name
/// and is required by some devices e.g. iOS). The font file must reside in the src root of the project in
/// order to be detectable. The file name should contain no slashes or any such value.
Expand All @@ -323,7 +341,8 @@ public static Font createTrueTypeFont(String fontName, float size, byte sizeUnit
///
/// - `fontName`: the name of the font
///
/// - `fileName`: the file name of the font as it appears in the src directory of the project, it MUST end with the .ttf extension!
/// - `fileName`: the file name of the font as it appears in the src directory of the project, it MUST end
/// with the .ttf or .otf extension!
///
/// #### Returns
///
Expand All @@ -339,8 +358,8 @@ public static Font createTrueTypeFont(String fontName, String fileName) {
return null;
}
} else {
if (fileName != null && (fileName.indexOf('/') > -1 || fileName.indexOf('\\') > -1 || !fileName.endsWith(".ttf"))) {
throw new IllegalArgumentException("The font file name must be relative to the root and end with ttf: " + fileName);
if (fileName != null && (fileName.indexOf('/') > -1 || fileName.indexOf('\\') > -1 || !isSupportedFontFile(fileName))) {
throw new IllegalArgumentException("The font file name must be relative to the root and end with .ttf or .otf: " + fileName);
}
Comment thread
Copilot marked this conversation as resolved.
}
Object font = Display.impl.loadTrueTypeFont(fontName, fileName);
Expand Down
25 changes: 23 additions & 2 deletions CodenameOne/src/com/codename1/ui/plaf/StyleParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,24 @@ private static FontInfo parseFontName(FontInfo out, String arg) {
return out;
}

/// True when the argument already names a font file rather than a bare
/// family, for either of the container extensions the runtime loads.
///
/// Uses endsWith rather than comparing indexOf against length - 4: for a
/// name shorter than the suffix both sides are -1, so a three character
/// family like "Foo" would look as though it already carried an extension
/// and would be left without one.
private static boolean hasFontFileSuffix(String arg) {
return endsWithIgnoreCase(arg, ".ttf") || endsWithIgnoreCase(arg, ".otf");
}

/// Case-insensitive suffix test that doesn't route through toLowerCase, so
/// the result can't depend on the default locale.
private static boolean endsWithIgnoreCase(String value, String suffix) {
return value != null && value.length() >= suffix.length()
&& value.regionMatches(true, value.length() - suffix.length(), suffix, 0, suffix.length());
}

private static FontInfo parseFontFile(FontInfo out, String arg) {
arg = arg.trim();
if (arg.indexOf('/') != -1) {
Expand All @@ -669,9 +687,12 @@ private static FontInfo parseFontFile(FontInfo out, String arg) {
if (arg.length() > 0 && arg.charAt(0) == '/') {
arg = arg.substring(1);
} else {
// A bare family name gets the default .ttf suffix, but an explicit
// .ttf/.otf is left alone -- appending to "Foo.otf" would ask for
// "Foo.otf.ttf" and lose the font.
arg = arg.indexOf("native:") == 0 ? arg :
arg.indexOf(".ttf") != arg.length() - 4 ? arg + ".ttf" :
arg;
hasFontFileSuffix(arg) ? arg :
arg + ".ttf";
}
out.setFile(arg);
return out;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,11 @@ public AddThemeEntry(boolean adding, EditableResources resources, ResourceEditor
String[] fontFiles = ResourceEditorView.getLoadedFile().getParentFile().list(new FilenameFilter() {
@Override
public boolean accept(File file, String string) {
return string.endsWith(".ttf");
// Same set the runtime loads (see Font.isSupportedFontFile):
// both container extensions, in any case. A .ttf-only filter
// here left resource-based themes unable to pick an OpenType
// font the API otherwise accepts.
return endsWithIgnoreCase(string, ".ttf") || endsWithIgnoreCase(string, ".otf");
}
});
if(fontFiles == null) {
Expand Down Expand Up @@ -3096,4 +3100,10 @@ public void run() {
private javax.swing.JSpinner trueTypeFontSizeValue;
private javax.swing.JButton videoTutorial;
// End of variables declaration//GEN-END:variables

/** Locale-independent case-insensitive suffix test. */
static boolean endsWithIgnoreCase(String value, String suffix) {
return value != null && value.length() >= suffix.length()
&& value.regionMatches(true, value.length() - suffix.length(), suffix, 0, suffix.length());
}
}
18 changes: 17 additions & 1 deletion Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -4502,8 +4502,9 @@ private void loadSkinFile(InputStream skin, final JFrame frm) {
e = z.getNextEntry();
continue;
}
if (name.endsWith(".ttf")) {
if (isBundledFontFile(name)) {
try {
// TRUETYPE_FONT covers the whole SFNT family, OpenType included.
java.awt.Font result = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, z);
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(result);
} catch (FontFormatException ex) {
Expand Down Expand Up @@ -12073,6 +12074,21 @@ private String nativeFontName(String fontName) {
return nativeFontNameForIOS(fontName, getAvailableFontNamesLowercase());
}

/**
* True for a font file bundled with the app. java.awt's TRUETYPE_FONT
* reads the whole SFNT family, so an OpenType file registers exactly like
* a TrueType one.
*/
private static boolean isBundledFontFile(String fileName) {
return endsWithIgnoreCase(fileName, ".ttf") || endsWithIgnoreCase(fileName, ".otf");
}

/** Locale-independent case-insensitive suffix test. */
private static boolean endsWithIgnoreCase(String value, String suffix) {
return value != null && value.length() >= suffix.length()
&& value.regionMatches(true, value.length() - suffix.length(), suffix, 0, suffix.length());
}

@Override
public Object loadTrueTypeFont(String fontName, String fileName) {
File fontFile = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9018,13 +9018,23 @@ public Object loadTrueTypeFont(String fontName, String fileName) {
// which is not reachable from the worker and truncates the data URL
// to 26 chars, so the old byte->dataURL approach 100% failed to load
// the font.
loadTrueTypeFont_(resolvedFontName, resolvedFileName, "truetype");
loadTrueTypeFont_(resolvedFontName, resolvedFileName, fontFormatOf(resolvedFileName));
loadedFonts.add(resolvedFontName);
}
return createFallbackTrueTypeFont(resolvedFontName, resolvedFileName);

}

/**
* The format hint handed to the FontFace constructor. Browsers treat
* "truetype" and "opentype" as the same SFNT container, but naming the
* actual one keeps the hint honest and leaves no room for a UA that decides
* to skip a source whose hint doesn't match.
*/
private static String fontFormatOf(String fileName) {
return fileName != null && fileName.toLowerCase().endsWith(".otf") ? "opentype" : "truetype";
}

private NativeFont createFallbackTrueTypeFont(String fontName, String fileName) {
NativeFont out = (NativeFont)createFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM);
out.fontName = nativeFontName(fontName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1775,7 +1775,7 @@ public Object loadTrueTypeFont(String fontName, String fileName) {
// executable via the DirectWrite in-memory loader so there is no file
// next to the exe. Falls back to the file-based loader (a font staged
// beside the exe) when the resource isn't embedded.
if (fileName != null && fileName.toLowerCase().endsWith(".ttf")) {
if (fileName != null && isBundledFontFile(fileName)) {
byte[] data = readResourceFully("/" + fileName);
if (data != null) {
font = LinuxNative.loadTrueTypeFontFromMemory(fontName, data);
Expand All @@ -1790,6 +1790,21 @@ public Object loadTrueTypeFont(String fontName, String fileName) {
return Long.valueOf(font);
}

/**
* True for a font the app bundles. FontConfig and FreeType read the SFNT
* container whether the outlines are glyf or CFF, so OpenType is as
* loadable as TrueType here.
*/
private static boolean isBundledFontFile(String fileName) {
return endsWithIgnoreCase(fileName, ".ttf") || endsWithIgnoreCase(fileName, ".otf");
}

/** Locale-independent case-insensitive suffix test. */
private static boolean endsWithIgnoreCase(String value, String suffix) {
return value != null && value.length() >= suffix.length()
&& value.regionMatches(true, value.length() - suffix.length(), suffix, 0, suffix.length());
}

/** Reads an embedded classpath resource fully into a byte[], or null. */
private byte[] readResourceFully(String resource) {
InputStream in = getResourceAsStream(LinuxImplementation.class, resource);
Expand Down
19 changes: 12 additions & 7 deletions Ports/WindowsPort/nativeSources/cn1_windows_text.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ static int cn1WinExeRelativePath(const wchar_t* fileName, wchar_t* out, int outL
return 1;
}

/* Case-insensitive ".ttf" suffix test. */
static int cn1WinIsTtf(const wchar_t* name) {
/* Case-insensitive ".ttf"/".otf" suffix test. DirectWrite reads the SFNT
* container whether the outlines are glyf or CFF, so both extensions name a
* font it can register. */
static int cn1WinIsFontFile(const wchar_t* name) {
if (name == NULL) {
return 0;
}
Expand All @@ -94,10 +96,13 @@ static int cn1WinIsTtf(const wchar_t* name) {
return 0;
}
const wchar_t* ext = name + (len - 4);
return ext[0] == L'.'
&& (ext[1] == L't' || ext[1] == L'T')
&& (ext[2] == L't' || ext[2] == L'T')
&& (ext[3] == L'f' || ext[3] == L'F');
if (ext[0] != L'.' || (ext[3] != L'f' && ext[3] != L'F')) {
return 0;
}
if ((ext[1] == L't' || ext[1] == L'T') && (ext[2] == L't' || ext[2] == L'T')) {
return 1;
}
return (ext[1] == L'o' || ext[1] == L'O') && (ext[2] == L't' || ext[2] == L'T');
}

/* Builds a CN1Font for an explicit family / pixel size / style. */
Expand Down Expand Up @@ -267,7 +272,7 @@ JAVA_LONG com_codename1_impl_windows_WindowsNative_loadTrueTypeFont___java_lang_
float dpi = cn1Win.dpiScale > 0.0f ? cn1Win.dpiScale : 1.0f;
CN1Font* font = NULL;

if (cn1WinIsTtf(fileName)) {
if (cn1WinIsFontFile(fileName)) {
wchar_t fullPath[MAX_PATH];
wchar_t registered[128];
registered[0] = L'\0';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1778,7 +1778,7 @@ public Object loadTrueTypeFont(String fontName, String fileName) {
// executable via the DirectWrite in-memory loader so there is no file
// next to the exe. Falls back to the file-based loader (a font staged
// beside the exe) when the resource isn't embedded.
if (fileName != null && fileName.toLowerCase().endsWith(".ttf")) {
if (fileName != null && isBundledFontFile(fileName)) {
byte[] data = readResourceFully("/" + fileName);
if (data != null) {
font = WindowsNative.loadTrueTypeFontFromMemory(fontName, data);
Expand All @@ -1793,6 +1793,21 @@ public Object loadTrueTypeFont(String fontName, String fileName) {
return Long.valueOf(font);
}

/**
* True for a font the app bundles. DirectWrite reads the SFNT container
* whether the outlines are glyf or CFF, so OpenType is as loadable as
* TrueType here.
*/
private static boolean isBundledFontFile(String fileName) {
return endsWithIgnoreCase(fileName, ".ttf") || endsWithIgnoreCase(fileName, ".otf");
}

/** Locale-independent case-insensitive suffix test. */
private static boolean endsWithIgnoreCase(String value, String suffix) {
return value != null && value.length() >= suffix.length()
&& value.regionMatches(true, value.length() - suffix.length(), suffix, 0, suffix.length());
}

/** Reads an embedded classpath resource fully into a byte[], or null. */
private byte[] readResourceFully(String resource) {
InputStream in = getResourceAsStream(WindowsImplementation.class, resource);
Expand Down
30 changes: 27 additions & 3 deletions Ports/iOSPort/nativeSources/IOSNative.m
Original file line number Diff line number Diff line change
Expand Up @@ -9808,10 +9808,34 @@ static void cn1RegisterBundledFontsOnce() {
}
cn1FontsRegistered = YES;
@autoreleasepool {
NSArray *fontPaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"ttf" inDirectory:nil];
for (NSString *fontPath in fontPaths) {
NSURL *url = [NSURL fileURLWithPath:fontPath];
// Core Text reads OpenType as readily as TrueType, and this scan is the
// only thing registering bundled fonts on watchOS -- its plist carries no
// UIAppFonts array -- so anything missed here falls back to the system
// font on the watch even though it renders everywhere else.
//
// List the resource directory once and compare the lower-cased extension,
// rather than calling pathsForResourcesOfType: per spelling: that call
// matches the extension exactly, so a per-spelling list only ever covers
// the spellings someone thought to write down while the rest of the stack
// accepts any case, and ".TtF" would be bundled and never registered.
//
// A shallow listing of the resource root is deliberate. Fonts are
// deployed flat next to theme.res -- that is why createTrueTypeFont
// forbids a path separator in the name -- so nothing is missed, and it
// avoids walking every resource in the bundle (pods, map assets, models)
// on the way to the first glyph.
NSString *resourceRoot = [[NSBundle mainBundle] resourcePath];
NSArray *resourceNames = resourceRoot == nil ? nil
: [[NSFileManager defaultManager] contentsOfDirectoryAtPath:resourceRoot error:NULL];
for (NSString *resourceName in resourceNames) {
NSString *ext = [[resourceName pathExtension] lowercaseString];
if (![ext isEqualToString:@"ttf"] && ![ext isEqualToString:@"otf"]) {
continue;
}
NSURL *url = [NSURL fileURLWithPath:[resourceRoot stringByAppendingPathComponent:resourceName]];
CFErrorRef error = NULL;
// A font already registered by UIAppFonts errors here; that is
// expected on iOS/tvOS and the error is discarded.
CTFontManagerRegisterFontsForURL((BRIDGE_CAST CFURLRef)url, kCTFontManagerScopeProcess, &error);
if (error != NULL) {
CFRelease(error);
Expand Down
Binary file added docs/demos/common/src/main/css/GuideRootFont.ttf
Binary file not shown.
51 changes: 51 additions & 0 deletions docs/demos/common/src/main/css/guide-snippets-theme.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/

/**
* Compiled developer-guide CSS snippets.
* Complete examples that do not require external assets or generated image borders live here.
Expand Down Expand Up @@ -419,6 +442,34 @@ MyLabel {
}
/* end::css-css-029[] */

/* A relative src URL resolves against the directory holding this CSS file, so
GuideRootFont.ttf sits beside it and GuideDemoFont-Bold.ttf sits in res/.
Both forms compile, which is the point the guide makes with this snippet. */
/* tag::css-css-044[] */
@font-face {
font-family: "GuideRootFont";
src: url(GuideRootFont.ttf);
}

@font-face {
font-family: "GuideDemoFont Bold";
src: url(res/GuideDemoFont-Bold.ttf);
}
/* end::css-css-044[] */

/* Both families come from the snippet above, so this fixture also proves the
root-level font actually resolves and ships: a @font-face is only copied to
the build output when some style references it. */
/* tag::css-css-045[] */
Default {
font-family: "GuideRootFont";
}

Title {
font-family: "GuideDemoFont Bold";
}
/* end::css-css-045[] */

/* tag::css-css-030[] */
@font-face {
font-family: "GuideDownloadedFont";
Expand Down
Loading
Loading