Skip to content

Commit 31ea4d6

Browse files
committed
eez-gui-lite support for 1, 2 and 4 bit fonts
1 parent 735f29e commit 31ea4d6

File tree

13 files changed

+506
-30
lines changed

13 files changed

+506
-30
lines changed

packages/eez-studio-types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@ export interface IWasmFlowRuntime {
830830
// EEZ-GUI Lite — font_data_t offsets
831831
_offsetofFontAscent(): number;
832832
_offsetofFontDescent(): number;
833+
_offsetofFontBpp(): number;
833834
_offsetofFontEncodingStart(): number;
834835
_offsetofFontEncodingEnd(): number;
835836
_offsetofFontGroups(): number;

packages/project-editor/eez-flow-lite/expression.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ export function buildExpression(
208208
return `String_format(&eezgui_ctx, ${JSON.stringify(formatStr)}, ${args.join(", ")})`;
209209
}
210210

211+
if (
212+
(node.operator == "==" || node.operator == "!=") &&
213+
node.left.valueType == "string" &&
214+
node.right.valueType == "string"
215+
) {
216+
return `(strcmp(${buildExpressionNode(node.left)}, ${buildExpressionNode(node.right)}) ${node.operator == "==" ? "=" : "!"}= 0)`
217+
}
218+
211219
return `(${buildExpressionNode(node.left)} ${node.operator} ${buildExpressionNode(node.right)})`;
212220
}
213221

packages/project-editor/eez-gui-lite/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ export class BuildEezGuiLite {
743743
build.blockStart(`static const eezgui_font_data_t font_${fontName} = {`);
744744
build.line(`.ascent = ${font.ascent},`);
745745
build.line(`.descent = ${font.descent},`);
746+
build.line(`.bpp = ${font.bpp},`);
746747
build.line(`.encodingStart = ${startEncoding},`);
747748
build.line(`.encodingEnd = ${endEncoding},`);
748749
build.line(`.groups = font_${fontName}_groups,`);

packages/project-editor/eez-gui-lite/page-runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ export class EezGuiLiteRuntime {
466466
// font_data_t field offsets
467467
const offFontAscent = this.wasm._offsetofFontAscent();
468468
const offFontDescent = this.wasm._offsetofFontDescent();
469+
const offFontBpp = this.wasm._offsetofFontBpp();
469470
const offFontEncodingStart = this.wasm._offsetofFontEncodingStart();
470471
const offFontEncodingEnd = this.wasm._offsetofFontEncodingEnd();
471472
const offFontGroups = this.wasm._offsetofFontGroups();
@@ -548,6 +549,7 @@ export class EezGuiLiteRuntime {
548549
const fontDataPtr = this.wasmMalloc(fontDataSize);
549550
this.wasm.HEAPU8[fontDataPtr + offFontAscent] = font.ascent;
550551
this.wasm.HEAPU8[fontDataPtr + offFontDescent] = font.descent;
552+
this.wasm.HEAPU8[fontDataPtr + offFontBpp] = font.bpp;
551553
this.wasm.HEAPU32[(fontDataPtr + offFontEncodingStart) >> 2] =
552554
startEncoding;
553555
this.wasm.HEAPU32[(fontDataPtr + offFontEncodingEnd) >> 2] =

packages/project-editor/features/font/font-extract/freetype.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,53 @@ export class ExtractFont implements IFontExtract {
256256
];
257257
}
258258
}
259+
} else if (
260+
this.params.bpp === 2 ||
261+
this.params.bpp === 4
262+
) {
263+
pixelArray = [];
264+
const bpp = this.params.bpp;
265+
const pixelsPerByte = 8 / bpp;
266+
const maxAlpha = (1 << bpp) - 1;
267+
const bytesPerLine = Math.floor(
268+
(glyphProperties.width * bpp + 7) / 8
269+
);
270+
271+
for (
272+
let y = this.glyphPosition.y + glyphInfoY1;
273+
y < this.glyphPosition.y + glyphInfoY2;
274+
y++
275+
) {
276+
for (
277+
let x = this.glyphPosition.x + glyphInfoX1,
278+
iByte = 0;
279+
iByte < bytesPerLine &&
280+
x < this.glyphPosition.x + glyphInfoX2;
281+
iByte++
282+
) {
283+
let byteData = 0;
284+
285+
for (
286+
let iPixel = 0;
287+
iPixel < pixelsPerByte &&
288+
x < this.glyphPosition.x + glyphInfoX2;
289+
iPixel++, x++
290+
) {
291+
const alpha =
292+
this.glyphPixelsArray[
293+
(y * this.canvasWidth + x) * 4 + 3
294+
];
295+
const quantized = Math.round(
296+
(alpha * maxAlpha) / 255
297+
);
298+
const shift =
299+
(pixelsPerByte - 1 - iPixel) * bpp;
300+
byteData |= quantized << shift;
301+
}
302+
303+
pixelArray.push(byteData);
304+
}
305+
}
259306
} else {
260307
pixelArray = [];
261308

packages/project-editor/features/font/font-extract/opentype.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,44 @@ export class ExtractFont implements IFontExtract {
167167
) {
168168
pixelArray.push(imageData.data[i * 4 + 3]);
169169
}
170+
} else if (this.params.bpp === 2 || this.params.bpp === 4) {
171+
const bpp = this.params.bpp;
172+
const pixelsPerByte = 8 / bpp;
173+
const maxAlpha = (1 << bpp) - 1;
174+
const bytesPerLine = Math.floor(
175+
(glyphProperties.width * bpp + 7) / 8
176+
);
177+
178+
for (let y = 0; y < glyphProperties.height; y++) {
179+
for (
180+
let x = 0, iByte = 0;
181+
iByte < bytesPerLine &&
182+
x < glyphProperties.width;
183+
iByte++
184+
) {
185+
let byteData = 0;
186+
187+
for (
188+
let iPixel = 0;
189+
iPixel < pixelsPerByte &&
190+
x < glyphProperties.width;
191+
iPixel++, x++
192+
) {
193+
const alpha =
194+
imageData.data[
195+
(y * glyphProperties.width + x) * 4 + 3
196+
];
197+
const quantized = Math.round(
198+
(alpha * maxAlpha) / 255
199+
);
200+
const shift =
201+
(pixelsPerByte - 1 - iPixel) * bpp;
202+
byteData |= quantized << shift;
203+
}
204+
205+
pixelArray.push(byteData);
206+
}
207+
}
170208
} else {
171209
let widthInBytes = Math.floor((glyphProperties.width + 7) / 8);
172210

packages/project-editor/features/font/font.tsx

Lines changed: 168 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,10 @@ export class Glyph extends EezObject {
479479

480480
if (font.bpp === 8 || project.projectTypeTraits.isLVGL) {
481481
ctx.globalAlpha = pixelValue / 255;
482+
} else if (font.bpp === 4) {
483+
ctx.globalAlpha = pixelValue / 15;
484+
} else if (font.bpp === 2) {
485+
ctx.globalAlpha = pixelValue / 3;
482486
}
483487

484488
if (pixelValue) {
@@ -835,12 +839,23 @@ export class Glyph extends EezObject {
835839
0
836840
);
837841

842+
const font = this.font;
843+
const maxAlpha = (1 << font.bpp) - 1;
838844
for (let x = 0; x < this.glyphBitmap.width; x++) {
839845
for (let y = 0; y < this.glyphBitmap.height; y++) {
840846
const i = (y * this.glyphBitmap.width + x) * 4;
841-
buffer[i + 0] = 255 - this.getPixel(x, y);
842-
buffer[i + 1] = 255 - this.getPixel(x, y);
843-
buffer[i + 2] = 255 - this.getPixel(x, y);
847+
const pixel = this.getPixel(x, y);
848+
const value =
849+
font.bpp === 8
850+
? pixel
851+
: font.bpp === 1
852+
? pixel
853+
? 255
854+
: 0
855+
: Math.round((pixel * 255) / maxAlpha);
856+
buffer[i + 0] = 255 - value;
857+
buffer[i + 1] = 255 - value;
858+
buffer[i + 2] = 255 - value;
844859
buffer[i + 3] = 255;
845860
}
846861
}
@@ -1264,9 +1279,148 @@ const ChangeBitsPerPixel = observer(
12641279
}
12651280
});
12661281

1282+
const newBpp = result.values.bpp;
1283+
1284+
if (font.bpp === newBpp) {
1285+
return;
1286+
}
1287+
1288+
// Group glyphs by their source file path and size.
1289+
// Glyphs with their own GlyphSource use that; others
1290+
// fall back to the font-level FontSource.
1291+
const sourceGroups = new Map<
1292+
string,
1293+
{
1294+
relativeFilePath: string;
1295+
size: number;
1296+
encodings: number[];
1297+
}
1298+
>();
1299+
1300+
for (const glyph of font.glyphs) {
1301+
const relPath =
1302+
glyph.source?.filePath || font.source?.filePath;
1303+
const size =
1304+
glyph.source?.size ||
1305+
font.source?.size ||
1306+
font.height;
1307+
1308+
if (!relPath) {
1309+
continue;
1310+
}
1311+
1312+
const key = `${relPath}\0${size}`;
1313+
let group = sourceGroups.get(key);
1314+
if (!group) {
1315+
group = {
1316+
relativeFilePath: relPath,
1317+
size,
1318+
encodings: []
1319+
};
1320+
sourceGroups.set(key, group);
1321+
}
1322+
group.encodings.push(glyph.encoding);
1323+
}
1324+
1325+
if (sourceGroups.size === 0) {
1326+
projectStore.updateObject(font, {
1327+
bpp: newBpp
1328+
});
1329+
return;
1330+
}
1331+
1332+
// Extract glyphs from each source file
1333+
const allNewGlyphs = new Map<
1334+
number,
1335+
{
1336+
x: number;
1337+
y: number;
1338+
width: number;
1339+
height: number;
1340+
dx: number;
1341+
glyphBitmap?: any;
1342+
}
1343+
>();
1344+
1345+
let firstFontProperties: any;
1346+
1347+
try {
1348+
for (const group of sourceGroups.values()) {
1349+
// Build encoding ranges from sorted encodings
1350+
const sorted = group.encodings.slice().sort(
1351+
(a, b) => a - b
1352+
);
1353+
const encodings: EncodingRange[] = [];
1354+
let i = 0;
1355+
while (i < sorted.length) {
1356+
const from = sorted[i];
1357+
while (
1358+
i + 1 < sorted.length &&
1359+
sorted[i + 1] === sorted[i] + 1
1360+
) {
1361+
i++;
1362+
}
1363+
encodings.push({ from, to: sorted[i] });
1364+
i++;
1365+
}
1366+
1367+
const fontProperties = await extractFont({
1368+
name: font.name,
1369+
absoluteFilePath:
1370+
projectStore.getAbsoluteFilePath(
1371+
group.relativeFilePath
1372+
),
1373+
embeddedFontFile: font.embeddedFontFile,
1374+
relativeFilePath: group.relativeFilePath,
1375+
renderingEngine: font.renderingEngine,
1376+
bpp: newBpp,
1377+
size: group.size,
1378+
threshold: font.threshold,
1379+
createGlyphs: true,
1380+
encodings,
1381+
createBlankGlyphs: false,
1382+
doNotAddGlyphIfNotFound: true
1383+
});
1384+
1385+
if (!firstFontProperties) {
1386+
firstFontProperties = fontProperties;
1387+
}
1388+
1389+
for (const g of fontProperties.glyphs) {
1390+
allNewGlyphs.set(g.encoding, g);
1391+
}
1392+
}
1393+
} catch (error) {
1394+
notification.error(
1395+
`Failed to change bits per pixel: ${error}`
1396+
);
1397+
return;
1398+
}
1399+
1400+
projectStore.undoManager.setCombineCommands(true);
1401+
12671402
projectStore.updateObject(font, {
1268-
bpp: result.values.bpp
1403+
bpp: firstFontProperties.bpp,
1404+
ascent: firstFontProperties.ascent,
1405+
descent: firstFontProperties.descent,
1406+
height: firstFontProperties.height
12691407
});
1408+
1409+
for (const glyph of font.glyphs) {
1410+
const newGlyphProps = allNewGlyphs.get(glyph.encoding);
1411+
if (newGlyphProps) {
1412+
projectStore.updateObject(glyph, {
1413+
x: newGlyphProps.x,
1414+
y: newGlyphProps.y,
1415+
width: newGlyphProps.width,
1416+
height: newGlyphProps.height,
1417+
dx: newGlyphProps.dx,
1418+
glyphBitmap: newGlyphProps.glyphBitmap
1419+
});
1420+
}
1421+
}
1422+
1423+
projectStore.undoManager.setCombineCommands(false);
12701424
};
12711425

12721426
render() {
@@ -1426,7 +1580,9 @@ export class Font extends EezObject {
14261580
readOnlyInPropertyGrid: true,
14271581
enumDisallowUndefined: true,
14281582
disabled: (font: Font) =>
1429-
(isNotV1Project(font) && !isLVGLProject(font)) ||
1583+
(isNotV1Project(font) &&
1584+
!isLVGLProject(font) &&
1585+
!isEezGuiLiteProject(font)) ||
14301586
(isLVGLProject(font) && font.lvglUseFreeType)
14311587
},
14321588
{
@@ -1436,14 +1592,15 @@ export class Font extends EezObject {
14361592
propertyGridFullRowComponent: ChangeBitsPerPixel,
14371593
skipSearch: true,
14381594
disabled: (font: Font) =>
1439-
!isLVGLProject(font) ||
1595+
(!isLVGLProject(font) && !isEezGuiLiteProject(font)) ||
14401596
(isLVGLProject(font) && font.lvglUseFreeType)
14411597
},
14421598
{
14431599
name: "threshold",
14441600
type: PropertyType.Number,
14451601
defaultValue: 128,
1446-
disabled: (font: Font) => isLVGLProject(font) || font.bpp == 8
1602+
disabled: (font: Font) =>
1603+
isLVGLProject(font) || font.bpp != 1
14471604
},
14481605
{
14491606
name: "height",
@@ -1977,8 +2134,10 @@ export class Font extends EezObject {
19772134
name: "bpp",
19782135
displayName: "Bits per pixel",
19792136
type: "enum",
1980-
enumItems: [1, 8],
1981-
visible: () => isV1Project(parent)
2137+
enumItems: [1, 2, 4, 8],
2138+
visible: () =>
2139+
isV1Project(parent) ||
2140+
isEezGuiLiteProject(parent)
19822141
},
19832142
{
19842143
name: "size",

0 commit comments

Comments
 (0)