mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-29 00:01:40 +01:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5ea114581 | ||
|
|
d555484826 | ||
|
|
b572cd9133 | ||
|
|
73437fe5c6 | ||
|
|
d1b67cd8d6 | ||
|
|
3d27495dcd | ||
|
|
5b1dbd0062 | ||
|
|
e3ab3925b9 | ||
|
|
b3a725c56a | ||
|
|
c4ff621d7e | ||
|
|
b7976e8b1f |
No files matched your search
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Browser editor checks
|
||||
run: |
|
||||
docker run --rm -v "$PWD:/src" -w /src mcr.microsoft.com/playwright:v1.63.0-noble \
|
||||
bash -lc 'mkdir -p /tmp/c4f-browser; cd /tmp/c4f-browser; npm init -y >/dev/null; npm install playwright@1.63.0; cd /src; NODE_PATH=/tmp/c4f-browser/node_modules node tests/browser_test.cjs'
|
||||
bash -lc 'mkdir -p /tmp/c4f-browser; cd /tmp/c4f-browser; npm init -y >/dev/null; npm install playwright@1.63.0; cd /src; NODE_PATH=/tmp/c4f-browser/node_modules node tests/browser_test.cjs && NODE_PATH=/tmp/c4f-browser/node_modules node tests/i18n_browser_test.cjs'
|
||||
|
||||
- name: Emulated gamepad checks
|
||||
run: |
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 1.3.1 — 2026-10-24
|
||||
|
||||
- Arabic and Brazilian Portuguese for the web interface and PS4 app.
|
||||
- Move, resize and hide face buttons individually, or keep them grouped.
|
||||
- Reset individual controls in the layout editor.
|
||||
- Updated Xbox-button troubleshooting for Chrome and Edge on Windows.
|
||||
|
||||
## 1.3.0 — 2026-10-22
|
||||
|
||||
WebRTC connections, controller testing on the PS4, and new experimental controller features.
|
||||
|
||||
@@ -64,6 +64,11 @@ sees a virtual DualShock 4, which provides:
|
||||
- **On your phone's home screen**, like an app.
|
||||
- **Stays awake**: your phone's screen doesn't turn off while you play.
|
||||
- **Gets through rest mode.** After the PS4 wakes up, open the page and pick your controller again.
|
||||
- **English, Brazilian Portuguese and Arabic.** The page follows the browser's language; change it at the bottom of
|
||||
the home screen or in **Settings → This device**. The PS4 app follows the console's system language; press
|
||||
**L1** on its main screen to switch between automatic, English, Português (Brasil) and العربية.
|
||||
Arabic menus read from right to left while controller positions stay unchanged.
|
||||
See [Languages](docs/languages.md) for font loading and offline use.
|
||||
|
||||
## Controller connection
|
||||
|
||||
@@ -161,6 +166,12 @@ Found a bug? Install the diagnostic package from the latest release first, then
|
||||
report. If Control4Free is useful to you, you can help keep it going on
|
||||
[Liberapay](https://liberapay.com/MoHadiShibli/).
|
||||
|
||||
## Contributors
|
||||
|
||||
- [@NotDev-Kike](https://github.com/NotDev-Kike) — HDMI-CEC testing and feedback.
|
||||
- [@lucasmaraia](https://github.com/lucasmaraia) — Brazilian Portuguese translation for the web interface
|
||||
and PS4 app.
|
||||
|
||||
## Credits
|
||||
|
||||
Control4Free's virtual-device code is ported from [SplashDown](https://github.com/seregonwar/SplashDown) by
|
||||
|
||||
@@ -54,6 +54,21 @@
|
||||
of John "Nielk1" Klein's DualSense trigger effect generator
|
||||
(https://gist.github.com/Nielk1/6d54cc2c00d2201ccb8c2720ad7538db), MIT.
|
||||
|
||||
- Arabic text shaping uses [HarfBuzz 11.2.1](https://github.com/harfbuzz/harfbuzz/tree/11.2.1).
|
||||
Its copyright notices and permissive license terms are retained in
|
||||
`vendor/harfbuzz/COPYING` and `vendor/harfbuzz/src/ms-use/COPYING`.
|
||||
- Mixed-direction native text uses [SheenBidi 2.9.0](https://github.com/Tehreer/SheenBidi/tree/v2.9.0),
|
||||
Apache-2.0; see `vendor/sheenbidi/LICENSE`.
|
||||
- The offline fallback Arabic fonts are [Noto Sans Arabic 2.013](https://github.com/notofonts/arabic/releases/tag/NotoSansArabic-v2.013),
|
||||
Light and Regular, under the SIL Open Font License 1.1 in `vendor/noto-sans-arabic/OFL.txt`.
|
||||
Their provenance and hashes are recorded in that directory's `README.control4free.md`.
|
||||
- The PS4 app reads SST Arabic Light and Roman from the console's installed system
|
||||
fonts; the web page requests those same fonts from the local PS4 service. SST is
|
||||
[Sony's typeface, developed with Monotype](https://www.sony.com/en/SonyInfo/design/stories/sst-font/).
|
||||
No SST font files are included in the
|
||||
source, packages or standalone page. The bundled Noto fonts remain available if
|
||||
the console's fonts cannot be loaded.
|
||||
|
||||
These license notices are also included inside the package.
|
||||
|
||||
The HTTP, ICE/STUN and SCTP transport code is implemented in this project;
|
||||
|
||||
+1586
-328
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -2,8 +2,8 @@
|
||||
#
|
||||
# Holds clang/lld plus a pinned ps4-payload-sdk release, so a build needs nothing
|
||||
# installed on the host but Docker. Matches the SDK's own CI (clang-18 / lld-18).
|
||||
# Docker's official Ubuntu mirror avoids Docker Hub's shared pull quota.
|
||||
FROM public.ecr.aws/docker/library/ubuntu:24.04@sha256:534baea6a22c03a63003dbc8dbe78fe34bc0d7e595d9a9dc9834884ff530eb55
|
||||
# Use the official registry; the public ECR mirror can reject CI pulls with 429.
|
||||
FROM docker.io/library/ubuntu:24.04@sha256:534baea6a22c03a63003dbc8dbe78fe34bc0d7e595d9a9dc9834884ff530eb55
|
||||
|
||||
ARG SDK_VERSION=v0.9
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# The official release contains the same OpenOrbis files as the former pinned
|
||||
# toolchain image. Fetch it directly so this build needs no Docker Hub pulls.
|
||||
# toolchain image. Fetch it directly and verify the archive checksum.
|
||||
FROM control4free-build:latest AS openorbis
|
||||
ARG OPENORBIS_VERSION=v0.5.3
|
||||
ARG OPENORBIS_ARCHIVE=toolchain-llvm-18.2.zip
|
||||
@@ -12,8 +12,8 @@ RUN curl -fsSL --retry 3 -o /tmp/openorbis.zip \
|
||||
&& tar -xzf /tmp/openorbis/toolchain-llvm-18.tar.gz -C /opt \
|
||||
&& test -f /opt/OpenOrbis/PS4Toolchain/lib/crt1.o \
|
||||
&& rm -rf /tmp/openorbis /tmp/openorbis.zip
|
||||
# Docker's official Ubuntu mirror, pinned to the same image as Docker Hub.
|
||||
FROM public.ecr.aws/docker/library/ubuntu:20.04@sha256:8feb4d8ca5354def3d8fce243717141ce31e2c428701f6682bd2fafe15388214 AS pkgcompat
|
||||
# Keep packaging compatibility pinned while using the official registry.
|
||||
FROM docker.io/library/ubuntu:20.04@sha256:8feb4d8ca5354def3d8fce243717141ce31e2c428701f6682bd2fafe15388214 AS pkgcompat
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libssl1.1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
FROM control4free-build:latest
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Languages
|
||||
|
||||
The web interface and PS4 app include English, Brazilian Portuguese and Arabic.
|
||||
Translations are included with Control4Free. They work without an internet connection
|
||||
and do not use an online translation service.
|
||||
|
||||
## Choose a language
|
||||
|
||||
On the web, use the language picker at the bottom of the home screen or in
|
||||
**Settings → This device**. Automatic follows the browser's primary language.
|
||||
The choice is saved on that phone or PC and applies to saved copies of the page too.
|
||||
|
||||
In the PS4 app, press **L1** on the launcher home screen to cycle through automatic,
|
||||
English, Português (Brasil) and العربية. Automatic follows the PS4 system language.
|
||||
The app saves its choice separately from the browser.
|
||||
|
||||
Arabic menus and text read from right to left. Numbers use **0–9**. The controller
|
||||
diagram, physical button positions, stick directions, RGB slider direction, addresses
|
||||
and technical readings keep their usual orientation. Menu navigation follows the
|
||||
visible positions, including wrapping at the edges of the tester's tool grid.
|
||||
|
||||
Names are displayed as entered. Changing languages does not rename users, connected
|
||||
gamepads or saved profiles. Errors that Control4Free does not recognize stay readable
|
||||
in English.
|
||||
|
||||
## Arabic fonts
|
||||
|
||||
The PS4 app reads the console's installed **SST Arabic Light and Roman** fonts.
|
||||
The Arabic web page loads those same fonts from the local PS4 service. This uses the
|
||||
home network, without contacting a font website. A saved HTML page requests them
|
||||
from the console address you enter.
|
||||
|
||||
Both interfaces also include **Noto Sans Arabic Light and Regular**. These display
|
||||
Arabic immediately while the PS4 font loads and remain available if it cannot be
|
||||
read or the console is unreachable. Sony's font files are not included in downloads.
|
||||
|
||||
## Implementation
|
||||
|
||||
Interface text uses explicit message keys. Dynamic messages translate their template
|
||||
before inserting names and other values. Values are plain text and use directional
|
||||
isolation in mixed Arabic/Latin messages. Translations and names are never interpreted
|
||||
as HTML.
|
||||
|
||||
The native app uses statically linked HarfBuzz for Arabic joining and mark positioning,
|
||||
SheenBidi for mixed-direction text, and the existing stb rasterizer for glyph drawing.
|
||||
Text measurement, wrapping and drawing use the same layout. Bounded layout and glyph
|
||||
caches belong to the drawing thread; controller input is sampled independently.
|
||||
|
||||
Only the two fixed font endpoints are available: `/fonts/arabic-light.otf` and
|
||||
`/fonts/arabic-regular.otf`. They read a bounded pair of system font files at service
|
||||
startup. Requests cannot select filesystem paths. See [Third-party material](../THIRD_PARTY.md)
|
||||
for the bundled fonts and text library licenses.
|
||||
+10
-2
@@ -49,8 +49,16 @@ The controller screen is a DualShock 4 laid out for your screen, best held sidew
|
||||
## Your own layout
|
||||
|
||||
Open the menu (**☰**) and choose **Edit the button layout**. Drag a button to move it, change its size, or
|
||||
hide it. **Reset to default** puts everything back; tap it twice, so a stray tap can't throw a layout away. Each device keeps its own layout, separately for portrait
|
||||
and landscape.
|
||||
hide it. Select the face buttons to reveal **Move buttons** beside **Size**. Choose **Separately** to move,
|
||||
resize or hide Cross, Square, Circle and Triangle individually, or **Together** to adjust the group.
|
||||
Switching between the two keeps your arrangement. **Done** saves it; **Cancel** discards the current edits.
|
||||
|
||||
The selected control has its own **Reset** button beside **Hide**. It restores that control's default
|
||||
position, size and visibility. In separate mode, resetting a face button leaves the other three in place;
|
||||
in together mode, it restores the face-button group. **Done** saves a reset, and **Cancel** undoes it.
|
||||
|
||||
**Reset to default** puts everything back; tap it twice, so a stray tap can't throw a layout away.
|
||||
Each device keeps its own layout, separately for portrait and landscape.
|
||||
|
||||
## Keyboard
|
||||
|
||||
|
||||
+41
-31
@@ -111,43 +111,53 @@ of Bluetooth, or the other way round), can also help.
|
||||
**A gamepad button does nothing after changing its mapping.** Let go of it and press it again: a button held
|
||||
down while a gamepad starts playing waits until it's released.
|
||||
|
||||
**The Xbox button presses PS for another controller in Chrome or Edge on Windows.** The browser's
|
||||
Windows.Gaming.Input backend combines Xbox/Guide presses from several controllers and reports them on one
|
||||
gamepad. Other buttons can still belong to the correct gamepad. Check **Mapping → Gamepad Controls** on
|
||||
each device: if the wrong device's **B16** lights up, the browser has already lost the physical source.
|
||||
Control4Free cannot recover that source from the reported input. See the
|
||||
[Chromium implementation](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/device/gamepad/wgi_data_fetcher_win.cc#419).
|
||||
**The Xbox button presses PS for another controller in Chrome or Edge on Windows.** By default these
|
||||
browsers read Xbox controllers through Windows.Gaming.Input, which has no Xbox/Guide button. The browser
|
||||
reads that button separately for all Xbox controllers at once and reports the combined press on its
|
||||
lowest-numbered one, whichever controller it came from. It happens with any two Xbox controllers, not
|
||||
only mixed generations. Other buttons still belong to the correct gamepad. Check
|
||||
**Mapping → Gamepad Controls** on each device: if the wrong device's **B16** lights up, the browser has
|
||||
already lost the physical source, and Control4Free cannot recover it from the reported input. See the
|
||||
[Chromium implementation](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/device/gamepad/wgi_data_fetcher_win.cc).
|
||||
|
||||
On Windows 11, newer Chrome builds offer an experimental **Windows GameInput** backend with
|
||||
per-device Guide-button callbacks. If your browser offers it, open
|
||||
`chrome://flags/#enable-windows-gameinput-data-fetcher` (or search `edge://flags` in Edge), set it to
|
||||
**Enabled**, and relaunch. Reconnect the gamepads and repeat the **Gamepad Controls** check: each Xbox
|
||||
button should light **B16** only on its own gamepad. Confirm both PS buttons after assigning controllers.
|
||||
This resolved combined Xbox/Guide reporting in a two-controller user test on Chrome 154 and Windows 11.
|
||||
Controller models and connection modes were not recorded, and other input and haptic capabilities were
|
||||
not revalidated. The option remains experimental; input counts and haptics may change. Compare
|
||||
**Gamepad Controls**, including any extra paddles, and review saved profiles after changing the backend.
|
||||
If gamepads stop appearing, return the flag to **Default**.
|
||||
See Chromium's [backend selection](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/device/gamepad/gamepad_platform_data_fetcher.h)
|
||||
and [per-device Guide handling](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/device/gamepad/gameinput_data_fetcher.cc).
|
||||
On Windows 11, Chrome and Edge also have an experimental **Windows GameInput** backend that reports each
|
||||
Xbox button on its own gamepad. It is off by default. Find your browser's version under
|
||||
`chrome://settings/help` (`edge://settings/help` in Edge), then turn GameInput on:
|
||||
|
||||
- **Version 155 or older:** open `chrome://flags/#enable-windows-gameinput-data-fetcher`
|
||||
(`edge://flags/#enable-windows-gameinput-data-fetcher` in Edge), set it to **Enabled**, and relaunch.
|
||||
- **Version 156 or 157:** the option is hidden because it has expired. Set
|
||||
`chrome://flags/#temporary-unexpire-flags-m155` (`edge://flags/#temporary-unexpire-flags-m155`) to
|
||||
**Enabled** and relaunch, then enable the GameInput option as above and relaunch again.
|
||||
- **Any version, including 158 and newer:** start the browser with
|
||||
`--enable-features=EnableWindowsGameInputDataFetcher`. Right-click the browser's shortcut, choose
|
||||
**Properties**, add a space and the option to the end of **Target**, and press **OK**. Close every
|
||||
browser window, check the notification area for a browser still running in the background, and open
|
||||
the browser from that shortcut. `chrome://version` (`edge://version`) lists the option under
|
||||
**Command Line** when it was applied.
|
||||
|
||||
Chromium hides expired options from the flags page, but the command-line option keeps working as long
|
||||
as the browser still contains the GameInput backend. See Chromium's
|
||||
[backend selection](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/device/gamepad/gamepad_platform_data_fetcher.h),
|
||||
[per-device Guide handling](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/device/gamepad/gameinput_data_fetcher.cc)
|
||||
and [flag expiry](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/flag_expiry.md).
|
||||
|
||||
Reconnect the gamepads and repeat the **Gamepad Controls** check: each Xbox button should light **B16**
|
||||
only on its own gamepad. Confirm both PS buttons after assigning controllers. This resolved combined
|
||||
Xbox/Guide reporting in a two-controller user test on Chrome 154 and Windows 11. Controller models and
|
||||
connection modes were not recorded, and other input and haptic capabilities were not revalidated. The
|
||||
backend remains experimental; input counts and haptics may change. Compare **Gamepad Controls**,
|
||||
including any extra paddles, and review saved profiles after changing the backend. If gamepads stop
|
||||
appearing, return the flag to **Default** or remove the option from the shortcut.
|
||||
|
||||
GameInput is a workaround for this browser's multi-controller Guide reporting, not a general
|
||||
Control4Free requirement. The controller page cannot select a Windows input backend or change browser
|
||||
flags through the [Gamepad API](https://w3c.github.io/gamepad/). A permanent correction to this browser
|
||||
path requires a browser fix; a future PC app could choose its own per-device reader. The existing mapper
|
||||
can also use an independently reported button for PS.
|
||||
path requires a browser fix; a future PC app could choose its own per-device reader.
|
||||
|
||||
In older browser builds that offer **Enable Windows.Gaming.Input**, open
|
||||
`chrome://flags/#enable-windows-gaming-input-data-fetcher` (or search `edge://flags` in Edge), set it to
|
||||
**Disabled**, and relaunch the browser. Reconnect the gamepads, select their controllers again, and check
|
||||
both Xbox buttons independently. This switches those builds to per-device XInput; extra paddles, input
|
||||
counts and haptic capabilities may change, so review saved mappings. The flag is unavailable in some
|
||||
newer builds.
|
||||
|
||||
If these options are unavailable or the raw Guide input still crosses devices, try Firefox on Windows
|
||||
or map an independently reported button to **PS button**
|
||||
using **DS4 Mapping → PS button → Change**. That replaces the Xbox/Guide binding. Remapping or swapping
|
||||
controller numbers alone cannot separate the combined Xbox-button signal.
|
||||
On Windows 10, or if none of these work, try Firefox on Windows or map an independently reported button
|
||||
to **PS button** using **DS4 Mapping → PS button → Change**. That replaces the Xbox/Guide binding.
|
||||
Remapping or swapping controller numbers alone cannot separate the combined Xbox-button signal.
|
||||
|
||||
**The gamepad does nothing.** Keep the page's window in front, press a button on the gamepad after the page
|
||||
has loaded, and check it has a controller under **Gamepads on this device**.
|
||||
|
||||
+11
-4
@@ -20,9 +20,14 @@ CC := clang-18
|
||||
CXX := clang++-18
|
||||
LD := ld.lld-18
|
||||
CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -O2 -Wall -Wextra -Werror -MMD -MP -D_DEFAULT_SOURCE -isysroot $(SDK) -isystem $(SDK)/include -I../vendor/jsmn -I../vendor/qrcodegen -I../vendor/stb -DC4F_LAUNCHER_VERSION='"$(VERSION)"' $(DIAG_FLAGS)
|
||||
OBJECTS += $(OUT)/main.o $(OUT)/input.o $(OUT)/navigation.o $(OUT)/screen.o $(OUT)/tester.o $(OUT)/tester_draw.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
|
||||
CFLAGS += -I../vendor/harfbuzz/src -I../vendor/sheenbidi/Headers
|
||||
OBJECTS += $(OUT)/main.o $(OUT)/input.o $(OUT)/navigation.o $(OUT)/screen.o $(OUT)/tester.o $(OUT)/tester_draw.o $(OUT)/draw.o $(OUT)/language.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
|
||||
OBJECTS += $(OUT)/text.o
|
||||
OBJECTS += $(OUT)/message.o
|
||||
TEXT_LIBS := ../build/text/libharfbuzz.a ../build/text/libsheenbidi.a
|
||||
LIBS := -lc -lkernel -lSceVideoOut -lScePad -lSceUserService -lSceSystemService -lSceNetCtl
|
||||
FONTS := ../vendor/roboto/Roboto-Light.ttf ../vendor/roboto/Roboto-Regular.ttf
|
||||
FONTS += ../vendor/noto-sans-arabic/NotoSansArabic-Light.ttf ../vendor/noto-sans-arabic/NotoSansArabic-Regular.ttf
|
||||
.PHONY: all package
|
||||
.DEFAULT_GOAL := all
|
||||
-include $(OBJECTS:.o=.d)
|
||||
@@ -40,10 +45,12 @@ $(OUT)/qrcodegen.o: ../vendor/qrcodegen/qrcodegen.c | $(OUT)
|
||||
$(OUT)/stb_truetype.o: stb_truetype.c | $(OUT)
|
||||
$(CC) $(CFLAGS) -std=gnu11 -w -c $< -o $@
|
||||
$(OUT)/fonts.c: $(FONTS) ../tools/embed_file.py | $(OUT)
|
||||
python3 ../tools/embed_file.py $@ c4fFontLight=../vendor/roboto/Roboto-Light.ttf c4fFontRegular=../vendor/roboto/Roboto-Regular.ttf
|
||||
python3 ../tools/embed_file.py $@ c4fFontLight=../vendor/roboto/Roboto-Light.ttf c4fFontRegular=../vendor/roboto/Roboto-Regular.ttf c4fFontArabicLight=../vendor/noto-sans-arabic/NotoSansArabic-Light.ttf c4fFontArabicRegular=../vendor/noto-sans-arabic/NotoSansArabic-Regular.ttf
|
||||
$(OUT)/fonts.o: $(OUT)/fonts.c
|
||||
$(CC) $(CFLAGS) -std=gnu11 -c $< -o $@
|
||||
$(OUT)/launcher.elf: $(OBJECTS)
|
||||
$(LD) -m elf_x86_64 -pie --script $(SDK)/link.x --eh-frame-hdr -o $@ $(OBJECTS) -L$(SDK)/lib $(LIBS) $(SDK)/lib/crt1.o
|
||||
$(TEXT_LIBS) &: ../tools/build_text.py $(shell find ../vendor/harfbuzz/src ../vendor/sheenbidi -type f)
|
||||
python3 ../tools/build_text.py --out ../build/text
|
||||
$(OUT)/launcher.elf: $(OBJECTS) $(TEXT_LIBS)
|
||||
$(LD) -m elf_x86_64 -pie --script $(SDK)/link.x --eh-frame-hdr -o $@ $(OBJECTS) $(TEXT_LIBS) -L$(SDK)/lib $(LIBS) $(SDK)/lib/crt1.o
|
||||
package: $(OUT)/launcher.elf $(PAYLOAD)
|
||||
python3 ../tools/package_launcher.py --sdk $(SDK) $(if $(C4F_DIAG),--diag)
|
||||
+17
-13
@@ -163,6 +163,9 @@ static char *c4fIniEdited(const char *text, int enable)
|
||||
return out;
|
||||
}
|
||||
|
||||
static C4fMessage c4fAutorunText;
|
||||
const C4fMessage *c4fAutorunMessage(void) { return &c4fAutorunText; }
|
||||
|
||||
/* ---- public ---- */
|
||||
|
||||
int c4fAutorunCheck(char *problem, size_t size)
|
||||
@@ -170,12 +173,13 @@ int c4fAutorunCheck(char *problem, size_t size)
|
||||
char path[300];
|
||||
size_t length = 0;
|
||||
problem[0] = 0;
|
||||
if (!c4fBundled) { snprintf(problem, size, "the bundled payload could not be read"); return C4F_AUTORUN_UNKNOWN; }
|
||||
if (c4fSandboxLeave()) { snprintf(problem, size, "GoldHEN did not let the app check (errno %d)", errno); return C4F_AUTORUN_UNKNOWN; }
|
||||
c4fMessageClear(&c4fAutorunText);
|
||||
if (!c4fBundled) { c4fMessageWrite(&c4fAutorunText, problem, size, "the bundled payload could not be read"); return C4F_AUTORUN_UNKNOWN; }
|
||||
if (c4fSandboxLeave()) { c4fMessageWrite(&c4fAutorunText, problem, size, "GoldHEN did not let the app check (errno %d)", errno); return C4F_AUTORUN_UNKNOWN; }
|
||||
int state = C4F_AUTORUN_OFF;
|
||||
char *ini = c4fPath(path, sizeof(path), "GoldHEN/payloads.ini") ? NULL : (char *)c4fReadFile(path, &length, 1u << 20);
|
||||
if (!ini && errno != ENOENT) {
|
||||
snprintf(problem, size, "Could not inspect AutoRun (errno %d)", errno); return C4F_AUTORUN_UNKNOWN;
|
||||
c4fMessageWrite(&c4fAutorunText, problem, size, "Could not inspect AutoRun (errno %d)", errno); return C4F_AUTORUN_UNKNOWN;
|
||||
}
|
||||
if (ini && c4fIniEnabled(ini) && !c4fPath(path, sizeof(path), "payloads/control4free.elf")) {
|
||||
size_t installedSize = 0;
|
||||
@@ -184,10 +188,10 @@ int c4fAutorunCheck(char *problem, size_t size)
|
||||
state = installedSize == c4fBundledSize && !memcmp(installed, c4fBundled, installedSize) ? C4F_AUTORUN_ON : C4F_AUTORUN_OUTDATED;
|
||||
else if (errno == ENOENT) {
|
||||
state = C4F_AUTORUN_OUTDATED;
|
||||
snprintf(problem, size, "Enabled, but the payload is missing; choose Update auto-start in Actions or press Triangle");
|
||||
c4fMessageWrite(&c4fAutorunText, problem, size, "Enabled, but the payload is missing; choose Update auto-start in Actions or press Triangle");
|
||||
} else {
|
||||
state = C4F_AUTORUN_UNKNOWN;
|
||||
snprintf(problem, size, "Enabled, but the payload cannot be inspected (errno %d)", errno);
|
||||
c4fMessageWrite(&c4fAutorunText, problem, size, "Enabled, but the payload cannot be inspected (errno %d)", errno);
|
||||
}
|
||||
free(installed);
|
||||
}
|
||||
@@ -199,13 +203,13 @@ int c4fAutorunEnable(char *message, size_t size)
|
||||
{
|
||||
char directory[300], path[300];
|
||||
size_t length = 0;
|
||||
if (!c4fBundled) { snprintf(message, size, "The bundled payload could not be read. Reinstall the Control4Free package."); return -1; }
|
||||
if (c4fSandboxLeave()) { snprintf(message, size, "GoldHEN did not let the app set up auto-start (errno %d).", errno); return -1; }
|
||||
if (!c4fBundled) { c4fMessageWrite(&c4fAutorunText, message, size, "The bundled payload could not be read. Reinstall the Control4Free package."); return -1; }
|
||||
if (c4fSandboxLeave()) { c4fMessageWrite(&c4fAutorunText, message, size, "GoldHEN did not let the app set up auto-start (errno %d).", errno); return -1; }
|
||||
int failed = c4fPath(directory, sizeof(directory), "payloads") || c4fPath(path, sizeof(path), "payloads/control4free.elf");
|
||||
if (!failed && mkdir(directory, 0777) && errno != EEXIST) failed = 1;
|
||||
if (!failed && c4fWriteFile(path, c4fBundled, c4fBundledSize)) failed = 1;
|
||||
if (failed) {
|
||||
snprintf(message, size, "Could not copy Control4Free to /data/payloads (errno %d).", errno);
|
||||
c4fMessageWrite(&c4fAutorunText, message, size, "Could not copy Control4Free to /data/payloads (errno %d).", errno);
|
||||
return -1;
|
||||
}
|
||||
failed = c4fPath(directory, sizeof(directory), "GoldHEN") || c4fPath(path, sizeof(path), "GoldHEN/payloads.ini");
|
||||
@@ -214,8 +218,8 @@ int c4fAutorunEnable(char *message, size_t size)
|
||||
if (!failed && !old && errno != ENOENT) failed = 1;
|
||||
char *edited = failed ? NULL : c4fIniEdited(old ? old : "", 1);
|
||||
if (!failed && (!edited || c4fWriteFile(path, edited, strlen(edited)))) failed = 1;
|
||||
if (failed) snprintf(message, size, "Could not add Control4Free to GoldHEN's AutoRun list (errno %d).", errno);
|
||||
else snprintf(message, size, "Auto-start is on. GoldHEN starts Control4Free each time it loads.");
|
||||
if (failed) c4fMessageWrite(&c4fAutorunText, message, size, "Could not add Control4Free to GoldHEN's AutoRun list (errno %d).", errno);
|
||||
else c4fMessageWrite(&c4fAutorunText, message, size, "Auto-start is on. GoldHEN starts Control4Free each time it loads.");
|
||||
free(old);
|
||||
free(edited);
|
||||
return failed ? -1 : 0;
|
||||
@@ -225,14 +229,14 @@ int c4fAutorunDisable(char *message, size_t size)
|
||||
{
|
||||
char path[300];
|
||||
size_t length = 0;
|
||||
if (c4fSandboxLeave()) { snprintf(message, size, "GoldHEN did not let the app change auto-start (errno %d).", errno); return -1; }
|
||||
if (c4fSandboxLeave()) { c4fMessageWrite(&c4fAutorunText, message, size, "GoldHEN did not let the app change auto-start (errno %d).", errno); return -1; }
|
||||
int failed = c4fPath(path, sizeof(path), "GoldHEN/payloads.ini");
|
||||
char *old = failed ? NULL : (char *)c4fReadFile(path, &length, 1u << 20);
|
||||
char *edited = old ? c4fIniEdited(old, 0) : NULL;
|
||||
if (!failed && old && (!edited || c4fWriteFile(path, edited, strlen(edited)))) failed = 1;
|
||||
if (!failed && !old && errno != ENOENT) failed = 1;
|
||||
if (failed) snprintf(message, size, "Could not change GoldHEN's AutoRun list (errno %d).", errno);
|
||||
else snprintf(message, size, "Auto-start is off. Start Control4Free here with Cross, or from GoldHEN's Payloader LaunchPad.");
|
||||
if (failed) c4fMessageWrite(&c4fAutorunText, message, size, "Could not change GoldHEN's AutoRun list (errno %d).", errno);
|
||||
else c4fMessageWrite(&c4fAutorunText, message, size, "Auto-start is off. Start Control4Free here with Cross, or from GoldHEN's Payloader LaunchPad.");
|
||||
free(old);
|
||||
free(edited);
|
||||
return failed ? -1 : 0;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* entry in /data/GoldHEN/payloads.ini, the files GoldHEN's Payloader LaunchPad
|
||||
* uses. The app leaves its sandbox for this through GoldHEN's SDK call. */
|
||||
#include <stddef.h>
|
||||
#include "message.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -19,6 +20,7 @@ const unsigned char *c4fAutorunBundled(size_t *size);
|
||||
int c4fAutorunCheck(char *problem, size_t size);
|
||||
int c4fAutorunEnable(char *message, size_t size);
|
||||
int c4fAutorunDisable(char *message, size_t size);
|
||||
const C4fMessage *c4fAutorunMessage(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
+65
-10
@@ -5,19 +5,41 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "stb_truetype.h"
|
||||
#include "text.h"
|
||||
|
||||
extern const unsigned char c4fFontLight[], c4fFontRegular[];
|
||||
|
||||
typedef struct { unsigned char *bitmap; int w, h, x0, y0, ready; float advance; } C4fGlyph;
|
||||
typedef struct { int font, size; float scale; C4fGlyph glyph[95]; } C4fFace;
|
||||
/* Printable ASCII, then Latin-1's letters and signs (160-255). */
|
||||
enum { C4F_GLYPHS = 95 + 96 };
|
||||
typedef struct { int font, size; float scale; C4fGlyph glyph[C4F_GLYPHS]; } C4fFace;
|
||||
|
||||
static stbtt_fontinfo c4fFonts[C4F_FONT_COUNT];
|
||||
static C4fFace c4fFaces[24];
|
||||
static int c4fFaceCount;
|
||||
static uint8_t c4fTextGamma[256];
|
||||
static const char *(*c4fTranslate)(const char *text);
|
||||
static unsigned c4fTranslation;
|
||||
|
||||
void c4fSetTextTranslation(const char *(*translate)(const char *text))
|
||||
{
|
||||
c4fTranslate = translate;
|
||||
c4fTranslation++;
|
||||
}
|
||||
|
||||
unsigned c4fTextTranslationGeneration(void) { return c4fTranslation; }
|
||||
void c4fTextFontChanged(void) { c4fTranslation++; }
|
||||
|
||||
/* The text as it is shown: its translation when there is one. */
|
||||
static const char *c4fShown(const char *text)
|
||||
{
|
||||
const char *shown = c4fTranslate && text ? c4fTranslate(text) : NULL;
|
||||
return shown ? shown : text;
|
||||
}
|
||||
|
||||
int c4fDrawInit(void)
|
||||
{
|
||||
if (c4fShapedTextInit() < 0) return -1;
|
||||
const unsigned char *data[C4F_FONT_COUNT] = { c4fFontLight, c4fFontRegular };
|
||||
for (int i = 0; i < C4F_FONT_COUNT; i++)
|
||||
if (!stbtt_InitFont(&c4fFonts[i], data[i], stbtt_GetFontOffsetForIndex(data[i], 0))) return -1;
|
||||
@@ -446,8 +468,11 @@ static C4fFace *c4fFace(int font, float size)
|
||||
int pixels = (int)(size + 0.5f);
|
||||
for (int i = 0; i < c4fFaceCount; i++)
|
||||
if (c4fFaces[i].font == font && c4fFaces[i].size == pixels) return &c4fFaces[i];
|
||||
if (font < 0 || font >= C4F_FONT_COUNT || c4fFaceCount == (int)(sizeof(c4fFaces) / sizeof(c4fFaces[0]))) return NULL;
|
||||
C4fFace *face = &c4fFaces[c4fFaceCount++];
|
||||
if (font < 0 || font >= C4F_FONT_COUNT) return NULL;
|
||||
static unsigned replace;
|
||||
const int capacity = (int)(sizeof(c4fFaces) / sizeof(c4fFaces[0]));
|
||||
C4fFace *face = c4fFaceCount < capacity ? &c4fFaces[c4fFaceCount++] : &c4fFaces[replace++ % capacity];
|
||||
for (int i = 0; i < C4F_GLYPHS; i++) free(face->glyph[i].bitmap);
|
||||
memset(face, 0, sizeof(*face));
|
||||
face->font = font;
|
||||
face->size = pixels;
|
||||
@@ -456,11 +481,24 @@ static C4fFace *c4fFace(int font, float size)
|
||||
return face;
|
||||
}
|
||||
|
||||
static int c4fCodepoint(unsigned char ch) { return ch < 32 || ch > 126 ? '?' : ch; }
|
||||
/* The next character of UTF-8 text, as one the faces carry: printable ASCII
|
||||
* or Latin-1. Anything else, or a broken sequence, shows as '?'. */
|
||||
static int c4fCodepoint(const char *text, int length, int *at)
|
||||
{
|
||||
const unsigned char *s = (const unsigned char *)text;
|
||||
int i = *at, ch = s[i++];
|
||||
if (ch >= 0xc0 && ch < 0xe0 && i < length && (s[i] & 0xc0) == 0x80) ch = ((ch & 0x1f) << 6) | (s[i++] & 0x3f);
|
||||
else if (ch >= 0x80) {
|
||||
while (i < length && (s[i] & 0xc0) == 0x80) i++;
|
||||
ch = '?';
|
||||
}
|
||||
*at = i;
|
||||
return ch < 32 || (ch > 126 && ch < 160) || ch > 255 ? '?' : ch;
|
||||
}
|
||||
|
||||
static C4fGlyph *c4fGlyph(C4fFace *face, int ch)
|
||||
{
|
||||
C4fGlyph *g = &face->glyph[ch - 32];
|
||||
C4fGlyph *g = &face->glyph[ch < 127 ? ch - 32 : ch - 160 + 95];
|
||||
if (!g->ready) {
|
||||
const stbtt_fontinfo *font = &c4fFonts[face->font];
|
||||
int advance, bearing, x0, y0, x1, y1;
|
||||
@@ -480,12 +518,14 @@ static C4fGlyph *c4fGlyph(C4fFace *face, int ch)
|
||||
|
||||
static float c4fTextWidthN(int font, float size, const char *text, int length)
|
||||
{
|
||||
if (c4fTextNeedsShaping(text, length))
|
||||
return c4fShapedText(NULL, font, size, 0, 0, (C4fColor){0}, text, length);
|
||||
C4fFace *face = c4fFace(font, size);
|
||||
float width = 0;
|
||||
int previous = 0;
|
||||
if (!face) return 0;
|
||||
for (int i = 0; i < length && text[i]; i++) {
|
||||
int ch = c4fCodepoint((unsigned char)text[i]);
|
||||
for (int i = 0; i < length && text[i];) {
|
||||
int ch = c4fCodepoint(text, length, &i);
|
||||
if (previous) width += stbtt_GetCodepointKernAdvance(&c4fFonts[font], previous, ch) * face->scale;
|
||||
width += c4fGlyph(face, ch)->advance;
|
||||
previous = ch;
|
||||
@@ -494,18 +534,25 @@ static float c4fTextWidthN(int font, float size, const char *text, int length)
|
||||
}
|
||||
|
||||
float c4fTextWidth(int font, float size, const char *text)
|
||||
{
|
||||
text = c4fShown(text);
|
||||
return c4fTextWidthRaw(font, size, text);
|
||||
}
|
||||
|
||||
float c4fTextWidthRaw(int font, float size, const char *text)
|
||||
{
|
||||
return c4fTextWidthN(font, size, text, (int)strlen(text));
|
||||
}
|
||||
|
||||
float c4fTextN(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text, int length)
|
||||
{
|
||||
if (c4fTextNeedsShaping(text, length)) return c4fShapedText(c, font, size, x, y, color, text, length);
|
||||
C4fFace *face = c4fFace(font, size);
|
||||
float pen = x;
|
||||
int previous = 0, baseline = (int)floorf(y + 0.5f);
|
||||
if (!face) return 0;
|
||||
for (int i = 0; i < length && text[i]; i++) {
|
||||
int ch = c4fCodepoint((unsigned char)text[i]);
|
||||
for (int i = 0; i < length && text[i];) {
|
||||
int ch = c4fCodepoint(text, length, &i);
|
||||
if (previous) pen += stbtt_GetCodepointKernAdvance(&c4fFonts[font], previous, ch) * face->scale;
|
||||
C4fGlyph *g = c4fGlyph(face, ch);
|
||||
if (g->bitmap) {
|
||||
@@ -523,6 +570,12 @@ float c4fTextN(C4fCanvas *c, int font, float size, float x, float y, C4fColor co
|
||||
}
|
||||
|
||||
float c4fText(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text)
|
||||
{
|
||||
text = c4fShown(text);
|
||||
return c4fTextRaw(c, font, size, x, y, color, text);
|
||||
}
|
||||
|
||||
float c4fTextRaw(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text)
|
||||
{
|
||||
return c4fTextN(c, font, size, x, y, color, text, (int)strlen(text));
|
||||
}
|
||||
@@ -531,6 +584,7 @@ int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float m
|
||||
float lineHeight, int maxLines, C4fColor color, const char *text)
|
||||
{
|
||||
int lines = 0;
|
||||
text = c4fShown(text);
|
||||
while (*text && lines < maxLines) {
|
||||
while (*text == ' ') text++;
|
||||
if (!*text) break;
|
||||
@@ -542,7 +596,8 @@ int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float m
|
||||
if (!text[end]) break;
|
||||
end++;
|
||||
}
|
||||
if (c) c4fTextN(c, font, size, x, y + lines * lineHeight, color, text, fit);
|
||||
float at = c4fTextIsRTL() ? x + maxWidth - c4fTextWidthN(font, size, text, fit) : x;
|
||||
if (c) c4fTextN(c, font, size, at, y + lines * lineHeight, color, text, fit);
|
||||
lines++;
|
||||
text += fit;
|
||||
}
|
||||
|
||||
+30
-1
@@ -4,6 +4,10 @@
|
||||
* Blending reads the destination, so never draw straight into the
|
||||
* write-combined framebuffer. */
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct { uint32_t *px; int w, h; } C4fCanvas;
|
||||
typedef struct { uint8_t r, g, b, a; } C4fColor;
|
||||
@@ -12,6 +16,17 @@ typedef struct { uint8_t r, g, b, a; } C4fColor;
|
||||
enum { C4F_FONT_LIGHT, C4F_FONT_REGULAR, C4F_FONT_COUNT };
|
||||
|
||||
int c4fDrawInit(void);
|
||||
/* Drawing-thread locale direction; shaping still works for Arabic user names
|
||||
* while the interface is English. ASCII technical values retain their order. */
|
||||
void c4fSetTextDirection(int rtl);
|
||||
int c4fTextIsRTL(void);
|
||||
/* Drawing thread only. Bytes remain owned by the caller and must outlive
|
||||
* drawing. A change clears Unicode caches and invalidates cached UI layers. */
|
||||
int c4fTextSetArabicFonts(const unsigned char *light, size_t lightSize,
|
||||
const unsigned char *regular, size_t regularSize);
|
||||
/* Prefer the installed PS4 SST Arabic faces. Returns 0 on success; -1 retains
|
||||
* the redistributable bundled font. Retry after leaving the sandbox if needed. */
|
||||
int c4fTextLoadSystemFonts(void);
|
||||
|
||||
/* Signed distance to a rounded box: centre, half size, corner radius. */
|
||||
float c4fRoundBoxDistance(float px, float py, float cx, float cy, float hx, float hy, float r);
|
||||
@@ -62,16 +77,30 @@ void c4fFillPath(C4fCanvas *c, const C4fPath *p, C4fPlacement at, C4fColor color
|
||||
/* The mask blurred and spread: a soft halo or drop shadow under a shape. */
|
||||
void c4fShadowPath(C4fCanvas *c, const C4fPath *p, C4fPlacement at, float dx, float dy, int radius, C4fColor color);
|
||||
|
||||
/* Text on a baseline. Sizes are CSS font-size in pixels. ASCII only. */
|
||||
/* Text on a baseline. Sizes are CSS font-size in pixels. UTF-8 Arabic uses
|
||||
* Unicode bidi layout and contextual shaping; Latin text keeps the Roboto
|
||||
* path. UI entry points resolve exact message keys before shaping. */
|
||||
float c4fText(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text);
|
||||
float c4fTextN(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text, int length);
|
||||
float c4fTextWidth(int font, float size, const char *text);
|
||||
/* User data uses the same renderer without looking it up as interface text. */
|
||||
float c4fTextRaw(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text);
|
||||
float c4fTextWidthRaw(int font, float size, const char *text);
|
||||
/* Word-wrapped text; returns the number of lines (drawn only if c is set). */
|
||||
int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float maxWidth,
|
||||
float lineHeight, int maxLines, C4fColor color, const char *text);
|
||||
|
||||
/* What to show in place of a text, or NULL to show it as it is. NULL turns
|
||||
* translation off. Each change bumps the generation, so cached drawings of
|
||||
* words know to redraw. */
|
||||
void c4fSetTextTranslation(const char *(*translate)(const char *text));
|
||||
unsigned c4fTextTranslationGeneration(void);
|
||||
|
||||
/* Corner label and two equally sized vector flags; paint into static layers.
|
||||
* `right` is the right edge, `top` the top of the 60 x 30 pixel flags. */
|
||||
void c4fDrawSolidarityBadge(C4fCanvas *c, int right, int top);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,533 @@
|
||||
/* Offline interface catalogs. Source message keys are looked up exactly,
|
||||
* before values are inserted, so user data is never translated. */
|
||||
#include "language.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include "draw.h"
|
||||
#include "screen.h"
|
||||
|
||||
/* libSceSystemService, declared here so this file needs no SDK header. */
|
||||
int sceSystemServiceParamGetInt(int id, int *value);
|
||||
#define C4F_SYSTEM_PARAM_LANG 1
|
||||
#define C4F_SYSTEM_LANG_PORTUGUESE 7
|
||||
#define C4F_SYSTEM_LANG_PORTUGUESE_BR 17
|
||||
#define C4F_SYSTEM_LANG_ARABIC 21
|
||||
|
||||
#define C4F_LANGUAGE_DIR "/data/control4free"
|
||||
#define C4F_LANGUAGE_FILE C4F_LANGUAGE_DIR "/launcher-language"
|
||||
|
||||
static const char *const c4fLanguageNames[C4F_LANGUAGE_COUNT] = { "auto", "en", "pt-BR", "ar" };
|
||||
static int c4fActiveLanguage = C4F_LANGUAGE_ENGLISH;
|
||||
|
||||
int c4fLanguageLoad(void)
|
||||
{
|
||||
char word[16] = {0};
|
||||
FILE *f = fopen(C4F_LANGUAGE_FILE, "r");
|
||||
if (!f) return C4F_LANGUAGE_AUTO;
|
||||
if (!fgets(word, sizeof(word), f)) word[0] = 0;
|
||||
fclose(f);
|
||||
word[strcspn(word, "\r\n ")] = 0;
|
||||
for (int i = 0; i < C4F_LANGUAGE_COUNT; i++)
|
||||
if (!strcmp(word, c4fLanguageNames[i])) return i;
|
||||
return C4F_LANGUAGE_AUTO;
|
||||
}
|
||||
|
||||
void c4fLanguageSave(int choice)
|
||||
{
|
||||
if (choice < 0 || choice >= C4F_LANGUAGE_COUNT) return;
|
||||
(void)mkdir(C4F_LANGUAGE_DIR, 0777);
|
||||
FILE *f = fopen(C4F_LANGUAGE_FILE, "w");
|
||||
if (!f) { printf("[c4f-launcher] could not save the language\n"); return; }
|
||||
fprintf(f, "%s\n", c4fLanguageNames[choice]);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static int c4fSystemLanguage(void)
|
||||
{
|
||||
int language = -1;
|
||||
if (sceSystemServiceParamGetInt(C4F_SYSTEM_PARAM_LANG, &language) < 0)
|
||||
return C4F_LANGUAGE_ENGLISH;
|
||||
if (language == C4F_SYSTEM_LANG_ARABIC) return C4F_LANGUAGE_ARABIC;
|
||||
if (language == C4F_SYSTEM_LANG_PORTUGUESE || language == C4F_SYSTEM_LANG_PORTUGUESE_BR)
|
||||
return C4F_LANGUAGE_PORTUGUESE;
|
||||
return C4F_LANGUAGE_ENGLISH;
|
||||
}
|
||||
|
||||
void c4fLanguageUse(int choice)
|
||||
{
|
||||
static int used = -1;
|
||||
int selected = choice == C4F_LANGUAGE_AUTO ? c4fSystemLanguage() : choice;
|
||||
if (selected <= C4F_LANGUAGE_AUTO || selected >= C4F_LANGUAGE_COUNT)
|
||||
selected = C4F_LANGUAGE_ENGLISH;
|
||||
if (selected == used) return;
|
||||
used = selected;
|
||||
c4fActiveLanguage = selected;
|
||||
c4fSetTextTranslation(selected == C4F_LANGUAGE_PORTUGUESE ? c4fPortugueseText :
|
||||
selected == C4F_LANGUAGE_ARABIC ? c4fArabicText : NULL);
|
||||
c4fSetTextDirection(selected == C4F_LANGUAGE_ARABIC);
|
||||
}
|
||||
|
||||
/* ---- the translations ---- */
|
||||
|
||||
/* Format conversions remain in the same order; callers format translated keys. */
|
||||
typedef struct { const char *en, *pt; } C4fPhrase;
|
||||
|
||||
static const C4fPhrase c4fPortuguese[] = {
|
||||
/* Main screen */
|
||||
{ "Not responding", "Sem resposta" },
|
||||
{ "Check Control4Free", "Verifique o Control4Free" },
|
||||
{ "Something answers on port 4264 but not the way this app expects. If an older Control4Free is running, stop it on its controller page or restart the PS4.",
|
||||
"Algo responde na porta 4264, mas não do jeito que este app espera. Se um Control4Free mais antigo estiver rodando, pare-o pela página do controle ou reinicie o PS4." },
|
||||
{ "App error", "Erro no app" },
|
||||
{ "Reopen the app", "Reabra o app" },
|
||||
{ "The app could not start properly. Close it with the PS button and open it again. Control4Free itself is not affected.",
|
||||
"O app não conseguiu iniciar direito. Feche-o com o botão PS e abra de novo. O Control4Free em si não foi afetado." },
|
||||
{ "Working", "Trabalhando" },
|
||||
{ "Please wait", "Aguarde" },
|
||||
{ "Checking", "Verificando" },
|
||||
{ "1 controller connected", "1 controle conectado" },
|
||||
{ "%d controllers connected", "%d controles conectados" },
|
||||
{ "Running", "Rodando" },
|
||||
{ "Control4Free is running", "O Control4Free está rodando" },
|
||||
{ "Open the address or scan the code on your phone or PC. Pick a controller there, then choose its user on the PS4.",
|
||||
"Abra o endereço ou escaneie o código no seu celular ou PC. Escolha um controle lá e depois o usuário dele no PS4." },
|
||||
{ "Stopped", "Parado" },
|
||||
{ "Start Control4Free", "Iniciar o Control4Free" },
|
||||
{ "It starts with GoldHEN after each restart. To start now, enable GoldHEN's PayLoader Server and press Cross.",
|
||||
"Ele inicia com o GoldHEN após cada reinício. Para iniciar agora, ative o PayLoader Server do GoldHEN e aperte X." },
|
||||
{ "Not running", "Não está rodando" },
|
||||
{ "Set up Control4Free", "Configurar o Control4Free" },
|
||||
{ "Press Cross to set up auto-start. To start Control4Free now, enable GoldHEN's PayLoader Server.",
|
||||
"Aperte X para configurar o início automático. Para iniciar o Control4Free agora, ative o PayLoader Server do GoldHEN." },
|
||||
{ "Open on your phone or PC", "Abra no seu celular ou PC" },
|
||||
{ "Connect the PS4 to your home network", "Conecte o PS4 à sua rede doméstica" },
|
||||
{ "Use the same network as the PS4. Touch, a keyboard and gamepads connected to the phone or PC all work.",
|
||||
"Use a mesma rede do PS4. Toque, teclado e gamepads conectados ao celular ou PC funcionam." },
|
||||
{ "Working...", "Trabalhando..." },
|
||||
{ "Scan with your phone's camera", "Escaneie com a câmera do celular" },
|
||||
{ "The code appears once it runs", "O código aparece quando ele estiver rodando" },
|
||||
{ "Installed %s; running %s", "Instalado %s; rodando %s" },
|
||||
{ "Press Square, then Cross to stop, and Cross again to start this app's version. All players disconnect.",
|
||||
"Aperte Quadrado e depois X para parar, e X de novo para iniciar a versão deste app. Todos os jogadores serão desconectados." },
|
||||
{ "Starts by itself with GoldHEN", "Inicia sozinho com o GoldHEN" },
|
||||
{ "GoldHEN starts Control4Free each time it loads. Closing this app leaves your controllers running. Turn off other controller plugins in your games.",
|
||||
"O GoldHEN inicia o Control4Free sempre que carrega. Fechar este app mantém os controles ativos. Desative outros plugins de controle." },
|
||||
{ "Auto-start uses an older Control4Free", "O início automático usa um Control4Free mais antigo" },
|
||||
{ "Press Triangle to update auto-start. GoldHEN uses this version after the next restart.",
|
||||
"Aperte Triângulo para atualizar o início automático. O GoldHEN usa esta versão após o próximo reinício." },
|
||||
{ "Auto-start is off", "Início automático desligado" },
|
||||
{ "Press Triangle to turn auto-start on. GoldHEN then starts Control4Free each time it loads.",
|
||||
"Aperte Triângulo para ligar o início automático. Aí o GoldHEN inicia o Control4Free toda vez que carrega." },
|
||||
{ "Auto-start status unknown", "Estado do início automático desconhecido" },
|
||||
{ "Checking with GoldHEN...", "Verificando com o GoldHEN..." },
|
||||
{ "TV remote: OK opens actions / arrows select / Back returns", "Controle da TV: OK abre as ações / setas escolhem / Voltar retorna" },
|
||||
{ "L1 Language: automatic", "L1 Idioma: automático" },
|
||||
{ "L1 Language: English", "L1 Idioma: English" },
|
||||
{ "L1 Language: Portuguese (Brazil)", "L1 Idioma: português (Brasil)" },
|
||||
{ "Start", "Iniciar" },
|
||||
{ "Set up", "Configurar" },
|
||||
{ "Turn off auto-start", "Desligar início automático" },
|
||||
{ "Update auto-start", "Atualizar início automático" },
|
||||
{ "Turn on auto-start", "Ligar início automático" },
|
||||
{ "Stop", "Parar" },
|
||||
{ "Test controllers", "Testar controles" },
|
||||
{ "Close", "Fechar" },
|
||||
|
||||
/* Actions and stopping */
|
||||
{ "Actions", "Ações" },
|
||||
{ "Working... You can return to the screen.", "Trabalhando... Você pode voltar à tela." },
|
||||
{ "Use arrows or the D-pad to choose an action.", "Use as setas ou o direcional para escolher uma ação." },
|
||||
{ "Select", "Selecionar" },
|
||||
{ "Back", "Voltar" },
|
||||
{ "TV remote: OK select / Back return", "Controle da TV: OK seleciona / Voltar retorna" },
|
||||
{ "Stop Control4Free?", "Parar o Control4Free?" },
|
||||
{ "Every controller disconnects. You can start Control4Free again from this screen.",
|
||||
"Todos os controles serão desconectados. Você pode iniciar o Control4Free de novo nesta tela." },
|
||||
{ "Cancel", "Cancelar" },
|
||||
{ "Arrows / D-pad: select OK / Cross: confirm Back / Circle: cancel",
|
||||
"Setas / direcional: escolher OK / X: confirmar Voltar / Círculo: cancelar" },
|
||||
{ "Resume", "Continuar" },
|
||||
{ "Set up and start", "Configurar e iniciar" },
|
||||
{ "Turn auto-start off", "Desligar o início automático" },
|
||||
{ "Turn auto-start on", "Ligar o início automático" },
|
||||
{ "Stop Control4Free", "Parar o Control4Free" },
|
||||
{ "Close app", "Fechar o app" },
|
||||
{ "Capture kernel log", "Capturar o log do kernel" },
|
||||
|
||||
/* What the app last did */
|
||||
{ "Checking Control4Free...", "Verificando o Control4Free..." },
|
||||
{ "Could not initialize launcher. Close with PS and retry.", "Não foi possível iniciar o app. Feche com o botão PS e tente de novo." },
|
||||
{ "Could not initialize input. Close with PS and retry.", "Não foi possível iniciar a entrada. Feche com o botão PS e tente de novo." },
|
||||
{ "Setting up Control4Free. Please wait...", "Configurando o Control4Free. Aguarde..." },
|
||||
{ "Starting Control4Free. Please wait...", "Iniciando o Control4Free. Aguarde..." },
|
||||
{ "Stopping and releasing controllers...", "Parando e liberando os controles..." },
|
||||
{ "Turning auto-start off...", "Desligando o início automático..." },
|
||||
{ "Turning auto-start on...", "Ligando o início automático..." },
|
||||
{ "Set up and running. GoldHEN will also start it after each restart.",
|
||||
"Configurado e rodando. O GoldHEN também vai iniciá-lo após cada reinício." },
|
||||
{ "Ready. Open the address on your phone or PC.", "Pronto. Abra o endereço no seu celular ou PC." },
|
||||
{ "Not running.", "Não está rodando." },
|
||||
{ "The app cannot reach Control4Free: %s", "O app não consegue alcançar o Control4Free: %s" },
|
||||
{ "No answer the app understands: %s", "Nenhuma resposta que o app entenda: %s" },
|
||||
{ "Already running. Open the address on your phone or PC.", "Já está rodando. Abra o endereço no seu celular ou PC." },
|
||||
{ "The app cannot check whether Control4Free is running (%s).", "O app não consegue verificar se o Control4Free está rodando (%s)." },
|
||||
{ "Control4Free is not responding (%s). Wait a moment after waking. If it stays stuck, restart the PS4.",
|
||||
"O Control4Free não está respondendo (%s). Aguarde um pouco depois de sair do modo de repouso. Se continuar travado, reinicie o PS4." },
|
||||
{ "The payload is missing. Reinstall the Control4Free package.", "O payload está faltando. Reinstale o pacote do Control4Free." },
|
||||
{ "The bundled payload is damaged. Reinstall the Control4Free package.", "O payload incluído está danificado. Reinstale o pacote do Control4Free." },
|
||||
{ "PayLoader did not answer (errno %d). Enable PayLoader Server in GoldHEN > Server Settings, then retry Start.",
|
||||
"O PayLoader não respondeu (errno %d). Ative o PayLoader Server em GoldHEN > Server Settings e tente Iniciar de novo." },
|
||||
{ "The transfer to PayLoader broke off. Retry Start.", "A transferência para o PayLoader foi interrompida. Tente Iniciar de novo." },
|
||||
{ "Sent, but Control4Free did not start (%s). Retry Start.", "Enviado, mas o Control4Free não iniciou (%s). Tente Iniciar de novo." },
|
||||
{ "Stopped. Ready to start again.", "Parado. Pronto para iniciar de novo." },
|
||||
{ "The app cannot reach Control4Free (%s). Stop it on its controller page.",
|
||||
"O app não consegue alcançar o Control4Free (%s). Pare-o pela página do controle." },
|
||||
{ "This copy does not answer the app (%s). Stop it on its controller page.",
|
||||
"Esta cópia não responde ao app (%s). Pare-a pela página do controle." },
|
||||
{ "Stop was not confirmed (%s). Check the controller page.", "A parada não foi confirmada (%s). Confira a página do controle." },
|
||||
{ "Still stopping. Wait a moment, then try again.", "Ainda parando. Aguarde um pouco e tente de novo." },
|
||||
{ "the status reply is not from Control4Free", "a resposta de status não é do Control4Free" },
|
||||
{ "Auto-start is on. %s", "Início automático ligado. %s" },
|
||||
{ "the bundled payload could not be read", "não foi possível ler o payload incluído" },
|
||||
{ "GoldHEN did not let the app check (errno %d)", "O GoldHEN não deixou o app verificar (errno %d)" },
|
||||
{ "Could not inspect AutoRun (errno %d)", "Não foi possível inspecionar o AutoRun (errno %d)" },
|
||||
{ "Enabled, but the payload is missing; choose Update auto-start in Actions or press Triangle",
|
||||
"Ativado, mas o payload está faltando; escolha Atualizar o início automático em Ações ou aperte Triângulo" },
|
||||
{ "Enabled, but the payload cannot be inspected (errno %d)", "Ativado, mas o payload não pode ser inspecionado (errno %d)" },
|
||||
{ "The bundled payload could not be read. Reinstall the Control4Free package.",
|
||||
"Não foi possível ler o payload incluído. Reinstale o pacote do Control4Free." },
|
||||
{ "GoldHEN did not let the app set up auto-start (errno %d).", "O GoldHEN não deixou o app configurar o início automático (errno %d)." },
|
||||
{ "Could not copy Control4Free to /data/payloads (errno %d).", "Não foi possível copiar o Control4Free para /data/payloads (errno %d)." },
|
||||
{ "Could not add Control4Free to GoldHEN's AutoRun list (errno %d).",
|
||||
"Não foi possível adicionar o Control4Free à lista de AutoRun do GoldHEN (errno %d)." },
|
||||
{ "Auto-start is on. GoldHEN starts Control4Free each time it loads.",
|
||||
"Início automático ligado. O GoldHEN inicia o Control4Free toda vez que carrega." },
|
||||
{ "GoldHEN did not let the app change auto-start (errno %d).", "O GoldHEN não deixou o app mudar o início automático (errno %d)." },
|
||||
{ "Could not change GoldHEN's AutoRun list (errno %d).", "Não foi possível mudar a lista de AutoRun do GoldHEN (errno %d)." },
|
||||
{ "Auto-start is off. Start Control4Free here with Cross, or from GoldHEN's Payloader LaunchPad.",
|
||||
"Início automático desligado. Inicie o Control4Free aqui com X, ou pelo Payloader LaunchPad do GoldHEN." },
|
||||
|
||||
/* Controller test */
|
||||
{ "Controller test", "Teste de controle" },
|
||||
{ "See the controller input received by this PS4. Select a user, then press buttons or move the sticks. PS and Share go to the PS4 itself and aren't shown here.",
|
||||
"Veja a entrada de controle que este PS4 recebe. Escolha um usuário e aperte botões ou mova os analógicos. PS e Share vão para o próprio PS4 e não aparecem aqui." },
|
||||
{ "Sticks", "Analógicos" },
|
||||
{ "Left", "Esquerdo" },
|
||||
{ "Right", "Direito" },
|
||||
{ "Triggers", "Gatilhos" },
|
||||
{ "Motion", "Movimento" },
|
||||
{ "Acceleration (G)", "Aceleração (G)" },
|
||||
{ "Gyro (rad/s)", "Giroscópio (rad/s)" },
|
||||
{ "Orientation", "Orientação" },
|
||||
{ "Light bar", "Barra de luz" },
|
||||
{ "Hold L3 + R3: menu TV remote: OK menu / Back leave", "Segure L3 + R3: menu Controle da TV: OK menu / Voltar sai" },
|
||||
{ "Waiting for the PS4's signed-in users...", "Aguardando os usuários conectados no PS4..." },
|
||||
{ "No touch", "Sem toque" },
|
||||
{ "Set by this test", "Definida por este teste" },
|
||||
{ "As the PS4 sets it", "Como o PS4 define" },
|
||||
{ "Controller page RTT", "RTT da página do controle" },
|
||||
{ "No controller page for this user", "Nenhuma página de controle para este usuário" },
|
||||
{ "C%d measuring...", "C%d medindo..." },
|
||||
{ "User %d", "Usuário %d" },
|
||||
{ "The PS4 refused that (0x%08x).", "O PS4 recusou isso (0x%08x)." },
|
||||
{ "No other user is signed in.", "Nenhum outro usuário está conectado." },
|
||||
{ "This user has no controller to test.", "Este usuário não tem controle para testar." },
|
||||
{ "Large motor: rumbling for a second.", "Motor grande: vibrando por um segundo." },
|
||||
{ "Small motor: rumbling for a second.", "Motor pequeno: vibrando por um segundo." },
|
||||
{ "Light bar reset.", "Barra de luz restaurada." },
|
||||
{ "Orientation reset.", "Orientação restaurada." },
|
||||
{ "Light bar set. It goes back to normal when you leave.", "Barra de luz definida. Ela volta ao normal quando você sair." },
|
||||
{ "This user's controller is unavailable.", "O controle deste usuário está indisponível." },
|
||||
{ "Switch user", "Trocar usuário" },
|
||||
{ "Rumble the large motor", "Vibrar o motor grande" },
|
||||
{ "Rumble the small motor", "Vibrar o motor pequeno" },
|
||||
{ "Choose the light-bar color", "Escolher a cor da barra de luz" },
|
||||
{ "Reset the light bar", "Restaurar a barra de luz" },
|
||||
{ "Reset orientation", "Restaurar orientação" },
|
||||
{ "Back to the launcher", "Voltar ao app" },
|
||||
{ "Return to live input", "Voltar à entrada ao vivo" },
|
||||
{ "Choose another player", "Escolher outro jogador" },
|
||||
{ "Recenter motion", "Recentralizar o movimento" },
|
||||
{ "Choose any RGB color", "Escolher qualquer cor RGB" },
|
||||
{ "One-second vibration", "Vibração de um segundo" },
|
||||
{ "Restore default color", "Restaurar a cor padrão" },
|
||||
{ "No other signed-in user", "Nenhum outro usuário conectado" },
|
||||
{ "Controller unavailable", "Controle indisponível" },
|
||||
{ "Large motor", "Motor grande" },
|
||||
{ "Small motor", "Motor pequeno" },
|
||||
{ "Reset light", "Restaurar luz" },
|
||||
{ "Leave test", "Sair do teste" },
|
||||
{ "Controller tools", "Ferramentas do controle" },
|
||||
{ "Mix red, green and blue. Changes preview on the selected controller.",
|
||||
"Misture vermelho, verde e azul. As mudanças aparecem no controle selecionado." },
|
||||
{ "Test motion, vibration and the light bar.", "Teste o movimento, a vibração e a barra de luz." },
|
||||
{ "D-pad or arrows to move", "Direcional ou setas para mover" },
|
||||
{ "Fast adjustment", "Ajuste rápido" },
|
||||
{ "Fine adjustment", "Ajuste fino" },
|
||||
{ "Done", "Concluído" },
|
||||
{ "Up/Down: channel / Left/Right: adjust / OK: step size / Back: done",
|
||||
"Cima/Baixo: canal / Esquerda/Direita: ajustar / OK: passo / Voltar: concluir" },
|
||||
{ "No one signed in", "Ninguém conectado" },
|
||||
{ "Controller connected", "Controle conectado" },
|
||||
{ "No controller connected", "Nenhum controle conectado" },
|
||||
{ "Signed out", "Desconectado" },
|
||||
{ "Cannot read (0x%08x)", "Não foi possível ler (0x%08x)" },
|
||||
{ "Opening controller...", "Abrindo o controle..." },
|
||||
{ "Waiting for a signed-in user", "Aguardando um usuário conectado" },
|
||||
{ "SELECTED USER", "USUÁRIO SELECIONADO" },
|
||||
{ "CONTROLLER", "CONTROLE" },
|
||||
{ "TEST OUTPUT", "SAÍDA DO TESTE" },
|
||||
{ "Light bar set by this test", "Barra de luz definida por este teste" },
|
||||
{ "Light bar: PS4 default", "Barra de luz: padrão do PS4" },
|
||||
{ "Large motor running", "Motor grande ligado" },
|
||||
{ "Small motor running", "Motor pequeno ligado" },
|
||||
{ "The light bar returns to its PS4 color when you leave the tester.", "A barra de luz volta à cor do PS4 quando você sai do teste." },
|
||||
{ "Choose a tool, or resume testing to see live input.", "Escolha uma ferramenta, ou continue o teste para ver a entrada ao vivo." },
|
||||
{ "Sign in another PS4 user to switch players here.", "Conecte outro usuário no PS4 para trocar de jogador aqui." },
|
||||
{ "Connect this user's controller to use this tool.", "Conecte o controle deste usuário para usar esta ferramenta." },
|
||||
{ "Red", "Vermelho" },
|
||||
{ "Green", "Verde" },
|
||||
{ "Blue", "Azul" },
|
||||
{ "COLOR PREVIEW", "PRÉVIA DA COR" },
|
||||
{ "Cross / OK switches fine and fast", "X / OK alterna entre fino e rápido" },
|
||||
{ "Step: %d", "Passo: %d" },
|
||||
{ "Reading this user's controller", "Lendo o controle deste usuário" },
|
||||
{ "The PS4 would not read it (0x%08x)", "O PS4 não conseguiu ler (0x%08x)" },
|
||||
{ "No one is signed in", "Ninguém está conectado" },
|
||||
{ "Opening this user's controller...", "Abrindo o controle deste usuário..." },
|
||||
{ "Rumbling: large motor", "Vibrando: motor grande" },
|
||||
{ "Rumbling: small motor", "Vibrando: motor pequeno" },
|
||||
{ "Hold L3 + R3 for two seconds for the menu: change user, rumble, light bar, leave.",
|
||||
"Segure L3 + R3 por dois segundos para o menu: trocar usuário, vibração, barra de luz, sair." },
|
||||
};
|
||||
|
||||
/* Exact interface keys only. Values are formatted after lookup by the caller. */
|
||||
static const C4fPhrase c4fArabic[] = {
|
||||
{ "Touchpad", "لوحة اللمس" },
|
||||
{ "Not responding", "لا يستجيب" },
|
||||
{ "Check Control4Free", "تحقق من Control4Free" },
|
||||
{ "Something answers on port 4264 but not the way this app expects. If an older Control4Free is running, stop it on its controller page or restart the PS4.", "توجد استجابة على المنفذ 4264 لا يتعرف عليها التطبيق. إذا كان إصدار أقدم من Control4Free يعمل، فأوقفه من صفحة التحكم أو أعد تشغيل PS4." },
|
||||
{ "App error", "خطأ في التطبيق" },
|
||||
{ "Reopen the app", "أعد فتح التطبيق" },
|
||||
{ "The app could not start properly. Close it with the PS button and open it again. Control4Free itself is not affected.", "تعذر بدء التطبيق. أغلقه باستخدام زر PS ثم افتحه مجددًا. لا يؤثر ذلك على خدمة Control4Free." },
|
||||
{ "Working", "جارٍ التنفيذ" },
|
||||
{ "Please wait", "يرجى الانتظار" },
|
||||
{ "Checking", "جارٍ التحقق" },
|
||||
{ "1 controller connected", "وحدة تحكم واحدة متصلة" },
|
||||
{ "%d controllers connected", "وحدات تحكم متصلة: %d" },
|
||||
{ "Running", "يعمل" },
|
||||
{ "Control4Free is running", "Control4Free يعمل" },
|
||||
{ "Open the address or scan the code on your phone or PC. Pick a controller there, then choose its user on the PS4.", "افتح العنوان أو امسح الرمز بهاتفك أو حاسوبك. اختر وحدة تحكم، ثم اختر مستخدمها على PS4." },
|
||||
{ "Stopped", "متوقف" },
|
||||
{ "Start Control4Free", "تشغيل Control4Free" },
|
||||
{ "It starts with GoldHEN after each restart. To start now, enable GoldHEN's PayLoader Server and press Cross.", "يبدأ مع GoldHEN بعد كل إعادة تشغيل. للبدء الآن، فعّل PayLoader Server في GoldHEN واضغط ×." },
|
||||
{ "Not running", "لا يعمل" },
|
||||
{ "Set up Control4Free", "إعداد Control4Free" },
|
||||
{ "Press Cross to set up auto-start. To start Control4Free now, enable GoldHEN's PayLoader Server.", "اضغط × لإعداد التشغيل التلقائي. لتشغيل Control4Free الآن، فعّل PayLoader Server في GoldHEN." },
|
||||
{ "Open on your phone or PC", "افتحه على هاتفك أو حاسوبك" },
|
||||
{ "Connect the PS4 to your home network", "صِل PS4 بشبكتك المنزلية" },
|
||||
{ "Use the same network as the PS4. Touch, a keyboard and gamepads connected to the phone or PC all work.", "استخدم شبكة PS4 نفسها. يمكنك اللعب باللمس أو بلوحة مفاتيح أو بوحدة تحكم متصلة بهاتفك أو حاسوبك." },
|
||||
{ "Working...", "جارٍ التنفيذ..." },
|
||||
{ "Scan with your phone's camera", "امسح الرمز بكاميرا هاتفك" },
|
||||
{ "The code appears once it runs", "يظهر الرمز عند تشغيل الخدمة" },
|
||||
{ "Installed %s; running %s", "المثبّت %s؛ قيد التشغيل %s" },
|
||||
{ "Press Square, then Cross to stop, and Cross again to start this app's version. All players disconnect.", "اضغط المربع ثم × للإيقاف، ثم × مجددًا لتشغيل إصدار هذا التطبيق. سينقطع اتصال جميع اللاعبين." },
|
||||
{ "Starts by itself with GoldHEN", "يبدأ تلقائيًا مع GoldHEN" },
|
||||
{ "GoldHEN starts Control4Free each time it loads. Closing this app leaves your controllers running. Turn off other controller plugins in your games.", "يبدأ GoldHEN خدمة Control4Free عند تحميله. تبقى وحدات التحكم متصلة بعد إغلاق التطبيق. عطّل إضافات التحكم الأخرى في ألعابك." },
|
||||
{ "Auto-start uses an older Control4Free", "التشغيل التلقائي يستخدم إصدارًا أقدم" },
|
||||
{ "Press Triangle to update auto-start. GoldHEN uses this version after the next restart.", "اضغط المثلث لتحديث التشغيل التلقائي. سيستخدم GoldHEN هذا الإصدار بعد إعادة التشغيل التالية." },
|
||||
{ "Auto-start is off", "التشغيل التلقائي معطّل" },
|
||||
{ "Press Triangle to turn auto-start on. GoldHEN then starts Control4Free each time it loads.", "اضغط المثلث لتفعيل التشغيل التلقائي. سيبدأ GoldHEN خدمة Control4Free عند تحميله." },
|
||||
{ "Auto-start status unknown", "حالة التشغيل التلقائي غير معروفة" },
|
||||
{ "Checking with GoldHEN...", "جارٍ التحقق من GoldHEN..." },
|
||||
{ "TV remote: OK opens actions / arrows select / Back returns", "ريموت التلفاز: OK للإجراءات / الأسهم للاختيار / رجوع للعودة" },
|
||||
{ "L1 Language: automatic", "L1 اللغة: تلقائي" },
|
||||
{ "L1 Language: English", "L1 اللغة: English" },
|
||||
{ "L1 Language: Portuguese (Brazil)", "L1 اللغة: البرتغالية (البرازيل)" },
|
||||
{ "Start", "تشغيل" },
|
||||
{ "Set up", "إعداد" },
|
||||
{ "Turn off auto-start", "تعطيل التشغيل التلقائي" },
|
||||
{ "Update auto-start", "تحديث التشغيل التلقائي" },
|
||||
{ "Turn on auto-start", "تفعيل التشغيل التلقائي" },
|
||||
{ "Stop", "إيقاف" },
|
||||
{ "Test controllers", "اختبار وحدات التحكم" },
|
||||
{ "Close", "إغلاق" },
|
||||
{ "Actions", "الإجراءات" },
|
||||
{ "Working... You can return to the screen.", "جارٍ التنفيذ... يمكنك العودة إلى الشاشة." },
|
||||
{ "Use arrows or the D-pad to choose an action.", "استخدم الأسهم أو أزرار الاتجاهات لاختيار إجراء." },
|
||||
{ "Select", "اختيار" },
|
||||
{ "Back", "رجوع" },
|
||||
{ "TV remote: OK select / Back return", "ريموت التلفاز: OK للاختيار / رجوع للعودة" },
|
||||
{ "Stop Control4Free?", "إيقاف Control4Free؟" },
|
||||
{ "Every controller disconnects. You can start Control4Free again from this screen.", "سينقطع اتصال جميع وحدات التحكم. يمكنك تشغيل Control4Free مجددًا من هذه الشاشة." },
|
||||
{ "Cancel", "إلغاء" },
|
||||
{ "Arrows / D-pad: select OK / Cross: confirm Back / Circle: cancel", "الأسهم / الاتجاهات: اختيار OK / ×: تأكيد رجوع / الدائرة: إلغاء" },
|
||||
{ "Resume", "متابعة" },
|
||||
{ "Set up and start", "إعداد وتشغيل" },
|
||||
{ "Turn auto-start off", "تعطيل التشغيل التلقائي" },
|
||||
{ "Turn auto-start on", "تفعيل التشغيل التلقائي" },
|
||||
{ "Stop Control4Free", "إيقاف Control4Free" },
|
||||
{ "Close app", "إغلاق التطبيق" },
|
||||
{ "Capture kernel log", "تسجيل سجل النواة" },
|
||||
{ "Checking Control4Free...", "جارٍ التحقق من Control4Free..." },
|
||||
{ "Could not initialize launcher. Close with PS and retry.", "تعذر بدء التطبيق. أغلقه بزر PS وحاول مجددًا." },
|
||||
{ "Could not initialize input. Close with PS and retry.", "تعذر بدء الإدخال. أغلق التطبيق بزر PS وحاول مجددًا." },
|
||||
{ "Setting up Control4Free. Please wait...", "جارٍ إعداد Control4Free. يرجى الانتظار..." },
|
||||
{ "Starting Control4Free. Please wait...", "جارٍ تشغيل Control4Free. يرجى الانتظار..." },
|
||||
{ "Stopping and releasing controllers...", "جارٍ الإيقاف وفصل وحدات التحكم..." },
|
||||
{ "Turning auto-start off...", "جارٍ تعطيل التشغيل التلقائي..." },
|
||||
{ "Turning auto-start on...", "جارٍ تفعيل التشغيل التلقائي..." },
|
||||
{ "Set up and running. GoldHEN will also start it after each restart.", "اكتمل الإعداد والخدمة تعمل. سيشغلها GoldHEN أيضًا بعد كل إعادة تشغيل." },
|
||||
{ "Ready. Open the address on your phone or PC.", "جاهز. افتح العنوان على هاتفك أو حاسوبك." },
|
||||
{ "Not running.", "لا يعمل." },
|
||||
{ "The app cannot reach Control4Free: %s", "يتعذر اتصال التطبيق بخدمة Control4Free: %s" },
|
||||
{ "No answer the app understands: %s", "لم يتعرف التطبيق على الاستجابة: %s" },
|
||||
{ "Already running. Open the address on your phone or PC.", "الخدمة تعمل بالفعل. افتح العنوان على هاتفك أو حاسوبك." },
|
||||
{ "The app cannot check whether Control4Free is running (%s).", "تعذر على التطبيق التحقق من تشغيل Control4Free (%s)." },
|
||||
{ "Control4Free is not responding (%s). Wait a moment after waking. If it stays stuck, restart the PS4.", "Control4Free لا يستجيب (%s). انتظر قليلًا بعد الخروج من وضع السكون. إذا استمرت المشكلة، فأعد تشغيل PS4." },
|
||||
{ "The payload is missing. Reinstall the Control4Free package.", "ملف الخدمة مفقود. أعد تثبيت حزمة Control4Free." },
|
||||
{ "The bundled payload is damaged. Reinstall the Control4Free package.", "ملف الخدمة المرفق تالف. أعد تثبيت حزمة Control4Free." },
|
||||
{ "PayLoader did not answer (errno %d). Enable PayLoader Server in GoldHEN > Server Settings, then retry Start.", "لم يستجب PayLoader (errno %d). فعّل PayLoader Server من GoldHEN > Server Settings، ثم حاول التشغيل مجددًا." },
|
||||
{ "The transfer to PayLoader broke off. Retry Start.", "انقطع الإرسال إلى PayLoader. حاول التشغيل مجددًا." },
|
||||
{ "Sent, but Control4Free did not start (%s). Retry Start.", "اكتمل الإرسال، لكن Control4Free لم يبدأ (%s). حاول التشغيل مجددًا." },
|
||||
{ "Stopped. Ready to start again.", "توقفت الخدمة. جاهزة للتشغيل مجددًا." },
|
||||
{ "The app cannot reach Control4Free (%s). Stop it on its controller page.", "يتعذر اتصال التطبيق بخدمة Control4Free (%s). أوقفها من صفحة التحكم." },
|
||||
{ "This copy does not answer the app (%s). Stop it on its controller page.", "هذه النسخة لا تستجيب للتطبيق (%s). أوقفها من صفحة التحكم." },
|
||||
{ "Stop was not confirmed (%s). Check the controller page.", "لم يتأكد الإيقاف (%s). تحقق من صفحة التحكم." },
|
||||
{ "Still stopping. Wait a moment, then try again.", "لا تزال الخدمة تتوقف. انتظر قليلًا وحاول مجددًا." },
|
||||
{ "the status reply is not from Control4Free", "استجابة الحالة ليست من Control4Free" },
|
||||
{ "Auto-start is on. %s", "التشغيل التلقائي مفعّل. %s" },
|
||||
{ "the bundled payload could not be read", "تعذرت قراءة ملف الخدمة المرفق" },
|
||||
{ "GoldHEN did not let the app check (errno %d)", "لم يسمح GoldHEN للتطبيق بالتحقق (errno %d)" },
|
||||
{ "Could not inspect AutoRun (errno %d)", "تعذر فحص AutoRun (errno %d)" },
|
||||
{ "Enabled, but the payload is missing; choose Update auto-start in Actions or press Triangle", "مفعّل، لكن ملف الخدمة مفقود؛ اختر تحديث التشغيل التلقائي من الإجراءات أو اضغط المثلث" },
|
||||
{ "Enabled, but the payload cannot be inspected (errno %d)", "مفعّل، لكن تعذر فحص ملف الخدمة (errno %d)" },
|
||||
{ "The bundled payload could not be read. Reinstall the Control4Free package.", "تعذرت قراءة ملف الخدمة المرفق. أعد تثبيت حزمة Control4Free." },
|
||||
{ "GoldHEN did not let the app set up auto-start (errno %d).", "لم يسمح GoldHEN بإعداد التشغيل التلقائي (errno %d)." },
|
||||
{ "Could not copy Control4Free to /data/payloads (errno %d).", "تعذر نسخ Control4Free إلى /data/payloads (errno %d)." },
|
||||
{ "Could not add Control4Free to GoldHEN's AutoRun list (errno %d).", "تعذرت إضافة Control4Free إلى قائمة AutoRun في GoldHEN (errno %d)." },
|
||||
{ "Auto-start is on. GoldHEN starts Control4Free each time it loads.", "التشغيل التلقائي مفعّل. يبدأ GoldHEN خدمة Control4Free عند تحميله." },
|
||||
{ "GoldHEN did not let the app change auto-start (errno %d).", "لم يسمح GoldHEN بتغيير التشغيل التلقائي (errno %d)." },
|
||||
{ "Could not change GoldHEN's AutoRun list (errno %d).", "تعذر تعديل قائمة AutoRun في GoldHEN (errno %d)." },
|
||||
{ "Auto-start is off. Start Control4Free here with Cross, or from GoldHEN's Payloader LaunchPad.", "التشغيل التلقائي معطّل. شغّل Control4Free هنا بزر × أو من Payloader LaunchPad في GoldHEN." },
|
||||
{ "Controller test", "اختبار وحدة التحكم" },
|
||||
{ "See the controller input received by this PS4. Select a user, then press buttons or move the sticks. PS and Share go to the PS4 itself and aren't shown here.", "شاهد الإدخال الذي يتلقاه PS4. اختر مستخدمًا ثم اضغط الأزرار أو حرّك العصوين. يذهب زرا PS وShare مباشرة إلى PS4 ولا يظهران هنا." },
|
||||
{ "Sticks", "العصوان" },
|
||||
{ "Left", "اليسرى" },
|
||||
{ "Right", "اليمنى" },
|
||||
{ "Triggers", "الزنادان" },
|
||||
{ "Motion", "الحركة" },
|
||||
{ "Acceleration (G)", "التسارع (G)" },
|
||||
{ "Gyro (rad/s)", "الجيروسكوب (rad/s)" },
|
||||
{ "Orientation", "الاتجاه" },
|
||||
{ "Light bar", "شريط الإضاءة" },
|
||||
{ "Hold L3 + R3: menu TV remote: OK menu / Back leave", "اضغط مطولًا L3 + R3: القائمة ريموت التلفاز: OK للقائمة / رجوع للخروج" },
|
||||
{ "Waiting for the PS4's signed-in users...", "بانتظار المستخدمين المسجّلين على PS4..." },
|
||||
{ "No touch", "لا يوجد لمس" },
|
||||
{ "Set by this test", "مضبوط من هذا الاختبار" },
|
||||
{ "As the PS4 sets it", "حسب إعداد PS4" },
|
||||
{ "Controller page RTT", "زمن اتصال صفحة التحكم" },
|
||||
{ "No controller page for this user", "لا توجد صفحة تحكم لهذا المستخدم" },
|
||||
{ "C%d measuring...", "C%d جارٍ القياس..." },
|
||||
{ "User %d", "المستخدم %d" },
|
||||
{ "The PS4 refused that (0x%08x).", "رفض PS4 هذا الإجراء (0x%08x)." },
|
||||
{ "No other user is signed in.", "لا يوجد مستخدم آخر مسجّل." },
|
||||
{ "This user has no controller to test.", "لا توجد وحدة تحكم لهذا المستخدم لاختبارها." },
|
||||
{ "Large motor: rumbling for a second.", "المحرك الكبير: اهتزاز لمدة ثانية." },
|
||||
{ "Small motor: rumbling for a second.", "المحرك الصغير: اهتزاز لمدة ثانية." },
|
||||
{ "Light bar reset.", "أُعيد ضبط شريط الإضاءة." },
|
||||
{ "Orientation reset.", "أُعيد ضبط الاتجاه." },
|
||||
{ "Light bar set. It goes back to normal when you leave.", "ضُبط شريط الإضاءة. سيعود إلى لونه المعتاد عند الخروج." },
|
||||
{ "This user's controller is unavailable.", "وحدة تحكم هذا المستخدم غير متاحة." },
|
||||
{ "Switch user", "تبديل المستخدم" },
|
||||
{ "Rumble the large motor", "تشغيل اهتزاز المحرك الكبير" },
|
||||
{ "Rumble the small motor", "تشغيل اهتزاز المحرك الصغير" },
|
||||
{ "Choose the light-bar color", "اختيار لون شريط الإضاءة" },
|
||||
{ "Reset the light bar", "إعادة ضبط شريط الإضاءة" },
|
||||
{ "Reset orientation", "إعادة ضبط الاتجاه" },
|
||||
{ "Back to the launcher", "العودة إلى المشغّل" },
|
||||
{ "Return to live input", "العودة إلى الإدخال المباشر" },
|
||||
{ "Choose another player", "اختيار لاعب آخر" },
|
||||
{ "Recenter motion", "إعادة توسيط الحركة" },
|
||||
{ "Choose any RGB color", "اختيار أي لون RGB" },
|
||||
{ "One-second vibration", "اهتزاز لمدة ثانية" },
|
||||
{ "Restore default color", "استعادة اللون الافتراضي" },
|
||||
{ "No other signed-in user", "لا يوجد مستخدم آخر مسجّل" },
|
||||
{ "Controller unavailable", "وحدة التحكم غير متاحة" },
|
||||
{ "Large motor", "المحرك الكبير" },
|
||||
{ "Small motor", "المحرك الصغير" },
|
||||
{ "Reset light", "إعادة ضبط اللون" },
|
||||
{ "Leave test", "مغادرة الاختبار" },
|
||||
{ "Controller tools", "أدوات وحدة التحكم" },
|
||||
{ "Mix red, green and blue. Changes preview on the selected controller.", "امزج الأحمر والأخضر والأزرق. يظهر اللون على وحدة التحكم المختارة." },
|
||||
{ "Test motion, vibration and the light bar.", "اختبر الحركة والاهتزاز وشريط الإضاءة." },
|
||||
{ "D-pad or arrows to move", "استخدم الاتجاهات أو الأسهم للتنقل" },
|
||||
{ "Fast adjustment", "تعديل سريع" },
|
||||
{ "Fine adjustment", "تعديل دقيق" },
|
||||
{ "Done", "تم" },
|
||||
{ "Up/Down: channel / Left/Right: adjust / OK: step size / Back: done", "أعلى/أسفل: القناة / يمين/يسار: تعديل / OK: مقدار التغيير / رجوع: إنهاء" },
|
||||
{ "No one signed in", "لا يوجد مستخدم مسجّل" },
|
||||
{ "Controller connected", "وحدة التحكم متصلة" },
|
||||
{ "No controller connected", "لا توجد وحدة تحكم متصلة" },
|
||||
{ "Signed out", "سُجّل الخروج" },
|
||||
{ "Cannot read (0x%08x)", "تعذرت القراءة (0x%08x)" },
|
||||
{ "Opening controller...", "جارٍ فتح وحدة التحكم..." },
|
||||
{ "Waiting for a signed-in user", "بانتظار مستخدم مسجّل" },
|
||||
{ "SELECTED USER", "المستخدم المختار" },
|
||||
{ "CONTROLLER", "وحدة التحكم" },
|
||||
{ "TEST OUTPUT", "خرج الاختبار" },
|
||||
{ "Light bar set by this test", "شريط الإضاءة مضبوط من الاختبار" },
|
||||
{ "Light bar: PS4 default", "شريط الإضاءة: إعداد PS4 الافتراضي" },
|
||||
{ "Large motor running", "المحرك الكبير يعمل" },
|
||||
{ "Small motor running", "المحرك الصغير يعمل" },
|
||||
{ "The light bar returns to its PS4 color when you leave the tester.", "يعود شريط الإضاءة إلى لون PS4 عند مغادرة الاختبار." },
|
||||
{ "Choose a tool, or resume testing to see live input.", "اختر أداة أو تابع الاختبار لمشاهدة الإدخال المباشر." },
|
||||
{ "Sign in another PS4 user to switch players here.", "سجّل دخول مستخدم آخر على PS4 للتبديل بين اللاعبين هنا." },
|
||||
{ "Connect this user's controller to use this tool.", "صِل وحدة تحكم هذا المستخدم لاستخدام الأداة." },
|
||||
{ "Red", "أحمر" },
|
||||
{ "Green", "أخضر" },
|
||||
{ "Blue", "أزرق" },
|
||||
{ "COLOR PREVIEW", "معاينة اللون" },
|
||||
{ "Cross / OK switches fine and fast", "× / OK للتبديل بين التعديل الدقيق والسريع" },
|
||||
{ "Step: %d", "مقدار التغيير: %d" },
|
||||
{ "Reading this user's controller", "جارٍ قراءة وحدة تحكم هذا المستخدم" },
|
||||
{ "The PS4 would not read it (0x%08x)", "لم يتمكن PS4 من قراءتها (0x%08x)" },
|
||||
{ "No one is signed in", "لا يوجد مستخدم مسجّل" },
|
||||
{ "Opening this user's controller...", "جارٍ فتح وحدة تحكم هذا المستخدم..." },
|
||||
{ "Rumbling: large motor", "اهتزاز: المحرك الكبير" },
|
||||
{ "Rumbling: small motor", "اهتزاز: المحرك الصغير" },
|
||||
{ "Hold L3 + R3 for two seconds for the menu: change user, rumble, light bar, leave.", "اضغط مطولًا L3 + R3 لمدة ثانيتين لفتح القائمة: المستخدم والاهتزاز والإضاءة والخروج." },
|
||||
{ "L1 Language: Arabic", "L1 اللغة: العربية" },
|
||||
};
|
||||
|
||||
static const char *c4fFind(const C4fPhrase *phrases, size_t count, const char *key)
|
||||
{
|
||||
if (!key) return NULL;
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
if (!strcmp(phrases[i].en, key)) return phrases[i].pt;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *c4fPortugueseText(const char *key)
|
||||
{
|
||||
if (key && !strcmp(key, "L1 Language: Arabic")) return "L1 Idioma: العربية";
|
||||
return c4fFind(c4fPortuguese, sizeof(c4fPortuguese) / sizeof(c4fPortuguese[0]), key);
|
||||
}
|
||||
|
||||
const char *c4fArabicText(const char *key)
|
||||
{
|
||||
return c4fFind(c4fArabic, sizeof(c4fArabic) / sizeof(c4fArabic[0]), key);
|
||||
}
|
||||
|
||||
const char *c4fTr(const char *key)
|
||||
{
|
||||
const char *translated = c4fActiveLanguage == C4F_LANGUAGE_PORTUGUESE ? c4fPortugueseText(key) :
|
||||
c4fActiveLanguage == C4F_LANGUAGE_ARABIC ? c4fArabicText(key) : NULL;
|
||||
return translated ? translated : key;
|
||||
}
|
||||
|
||||
int c4fLanguageIsArabic(void)
|
||||
{
|
||||
return c4fActiveLanguage == C4F_LANGUAGE_ARABIC;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef C4F_LAUNCHER_LANGUAGE_H
|
||||
#define C4F_LAUNCHER_LANGUAGE_H
|
||||
/* Offline interface catalogs. Translate a source message key before formatting
|
||||
* values into it. Saved language choices require the app to leave its sandbox. */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* The saved choice; C4F_LANGUAGE_AUTO when there is none. */
|
||||
int c4fLanguageLoad(void);
|
||||
void c4fLanguageSave(int choice);
|
||||
/* Shows every text in the language chosen. Automatic follows the PS4's system
|
||||
* language. */
|
||||
void c4fLanguageUse(int choice);
|
||||
/* Exact source-key lookups, returning NULL when a catalog has no entry. */
|
||||
const char *c4fPortugueseText(const char *key);
|
||||
const char *c4fArabicText(const char *key);
|
||||
/* Active-language lookup; an unknown key is returned unchanged. */
|
||||
const char *c4fTr(const char *key);
|
||||
int c4fLanguageIsArabic(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
+62
-18
@@ -14,7 +14,9 @@
|
||||
#include <orbis/UserService.h>
|
||||
#include <orbis/VideoOut.h>
|
||||
#include "autorun.h"
|
||||
#include "draw.h"
|
||||
#include "input.h"
|
||||
#include "language.h"
|
||||
#include "navigation.h"
|
||||
#include "sandbox.h"
|
||||
#include "screen.h"
|
||||
@@ -38,17 +40,22 @@ static int c4fConnectionService = C4F_SERVICE_UNKNOWN;
|
||||
static uint64_t c4fConnectionCheckedAt;
|
||||
|
||||
/* Set up: auto-start on, then start it now if PayLoader takes it. */
|
||||
static int c4fSetUp(char *message, size_t size)
|
||||
static int c4fSetUp(char *message, size_t size, C4fMessage *text)
|
||||
{
|
||||
int result = c4fAutorunEnable(message, size);
|
||||
*text = *c4fAutorunMessage();
|
||||
if (result) return result;
|
||||
char started[160];
|
||||
size_t payloadSize;
|
||||
const unsigned char *payload = c4fAutorunBundled(&payloadSize);
|
||||
result = c4fLauncherStart(payload, payloadSize, started, sizeof(started));
|
||||
if (result == 0) snprintf(message, size, "Set up and running. GoldHEN will also start it after each restart.");
|
||||
*text = *c4fLauncherMessage();
|
||||
if (result == 0) c4fMessageWrite(text, message, size, "Set up and running. GoldHEN will also start it after each restart.");
|
||||
else if (result == -2) snprintf(message, size, "%s", started);
|
||||
else snprintf(message, size, "Auto-start is on. %s", started);
|
||||
else {
|
||||
snprintf(message, size, "Auto-start is on. %s", started);
|
||||
text->prefix = "Auto-start is on. %s";
|
||||
}
|
||||
return result == -2 ? -2 : 0;
|
||||
}
|
||||
|
||||
@@ -57,6 +64,7 @@ static void *c4fWorker(void *)
|
||||
uint64_t nextCheck = 0;
|
||||
int first = 1, autorun = C4F_AUTORUN_UNKNOWN;
|
||||
char autorunNote[96] = {0}, shownProblem[128] = {0};
|
||||
C4fMessage autorunText = {};
|
||||
for (;;) {
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
int command = c4fCommand, quit = c4fQuit;
|
||||
@@ -65,6 +73,7 @@ static void *c4fWorker(void *)
|
||||
if (quit) return NULL;
|
||||
if (command || c4fLauncherTimeMs() >= nextCheck) {
|
||||
char message[160] = {0}, address[64] = {0};
|
||||
C4fMessage messageText = {};
|
||||
[[maybe_unused]] int result = 0; /* read by the diagnostic build */
|
||||
/* The console's own address: shown for the phone or PC, and the
|
||||
* app's second way to reach Control4Free from its sandbox. */
|
||||
@@ -84,15 +93,28 @@ static void *c4fWorker(void *)
|
||||
size_t payloadSize;
|
||||
const unsigned char *payload = c4fAutorunBundled(&payloadSize);
|
||||
result = c4fLauncherStart(payload, payloadSize, message, sizeof(message));
|
||||
messageText = *c4fLauncherMessage();
|
||||
}
|
||||
if (command == C4F_DO_STOP) result = c4fLauncherStop(message, sizeof(message));
|
||||
if (command == C4F_DO_SET_UP) result = c4fSetUp(message, sizeof(message));
|
||||
if (command == C4F_DO_STOP) {
|
||||
result = c4fLauncherStop(message, sizeof(message));
|
||||
messageText = *c4fLauncherMessage();
|
||||
}
|
||||
if (command == C4F_DO_SET_UP) result = c4fSetUp(message, sizeof(message), &messageText);
|
||||
#ifdef C4F_DIAG
|
||||
if (command == C4F_DO_START || command == C4F_DO_SET_UP) c4fDiagAfterStart(result, message);
|
||||
#endif
|
||||
if (command == C4F_DO_AUTORUN_ON) result = c4fAutorunEnable(message, sizeof(message));
|
||||
if (command == C4F_DO_AUTORUN_OFF) result = c4fAutorunDisable(message, sizeof(message));
|
||||
if (first || command >= C4F_DO_SET_UP) autorun = c4fAutorunCheck(autorunNote, sizeof(autorunNote));
|
||||
if (command == C4F_DO_AUTORUN_ON) {
|
||||
result = c4fAutorunEnable(message, sizeof(message));
|
||||
messageText = *c4fAutorunMessage();
|
||||
}
|
||||
if (command == C4F_DO_AUTORUN_OFF) {
|
||||
result = c4fAutorunDisable(message, sizeof(message));
|
||||
messageText = *c4fAutorunMessage();
|
||||
}
|
||||
if (first || command >= C4F_DO_SET_UP) {
|
||||
autorun = c4fAutorunCheck(autorunNote, sizeof(autorunNote));
|
||||
autorunText = *c4fAutorunMessage();
|
||||
}
|
||||
C4fServiceStatus status;
|
||||
uint64_t probeAt = c4fLauncherTimeMs();
|
||||
int running = c4fLauncherProbe(&status);
|
||||
@@ -100,12 +122,15 @@ static void *c4fWorker(void *)
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
if (command) {
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s", message);
|
||||
c4fScreen.messageText = messageText;
|
||||
} else if (!c4fScreen.locked &&
|
||||
(first || running != c4fScreen.running || (running < 0 && strcmp(problem, shownProblem)))) {
|
||||
if (running == 1) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Ready. Open the address on your phone or PC.");
|
||||
else if (running == 0) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Not running.");
|
||||
else if (!c4fLauncherReached()) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "The app cannot reach Control4Free: %s", problem);
|
||||
else snprintf(c4fScreen.message, sizeof(c4fScreen.message), "No answer the app understands: %s", problem);
|
||||
if (running == 1) c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message), "Ready. Open the address on your phone or PC.");
|
||||
else if (running == 0) c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message), "Not running.");
|
||||
else if (!c4fLauncherReached()) c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message), "The app cannot reach Control4Free: %s", problem);
|
||||
else c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message), "No answer the app understands: %s", problem);
|
||||
if (c4fScreen.messageText.argType == C4F_MESSAGE_STRING)
|
||||
c4fScreen.messageText.valueIsKey = c4fLauncherProblemIsKey();
|
||||
snprintf(shownProblem, sizeof(shownProblem), "%s", problem);
|
||||
}
|
||||
c4fScreen.running = running;
|
||||
@@ -116,6 +141,7 @@ static void *c4fWorker(void *)
|
||||
snprintf(c4fScreen.runningVersion, sizeof(c4fScreen.runningVersion), "%s", running == 1 ? status.version : "");
|
||||
c4fScreen.autorun = autorun;
|
||||
snprintf(c4fScreen.autorunNote, sizeof(c4fScreen.autorunNote), "%s", autorunNote);
|
||||
c4fScreen.autorunText = autorunText;
|
||||
snprintf(c4fScreen.address, sizeof(c4fScreen.address), "%s", address);
|
||||
/* A queued click while a periodic probe was in flight remains busy. */
|
||||
if (!c4fCommand) c4fScreen.busy = 0;
|
||||
@@ -425,6 +451,7 @@ static void *c4fInputWorker(void *)
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
int quit = c4fQuit || c4fCloseRequested;
|
||||
uint64_t renderedAt = c4fRenderAt;
|
||||
tester.rtl = c4fScreen.rtl;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
if (quit) break;
|
||||
/* A suspended or stalled display must not leave test output running.
|
||||
@@ -501,16 +528,18 @@ static void *c4fInputWorker(void *)
|
||||
else if (action == C4F_ACTION_START) {
|
||||
int setUp = c4fScreen.autorun != C4F_AUTORUN_ON;
|
||||
c4fCommand = setUp ? C4F_DO_SET_UP : C4F_DO_START; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message),
|
||||
setUp ? "Setting up Control4Free. Please wait..." : "Starting Control4Free. Please wait...");
|
||||
} else if (action == C4F_ACTION_STOP) {
|
||||
c4fCommand = C4F_DO_STOP; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
|
||||
c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
|
||||
} else if (action == C4F_ACTION_AUTORUN) {
|
||||
int off = c4fScreen.autorun == C4F_AUTORUN_ON;
|
||||
c4fCommand = off ? C4F_DO_AUTORUN_OFF : C4F_DO_AUTORUN_ON; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message),
|
||||
off ? "Turning auto-start off..." : "Turning auto-start on...");
|
||||
} else if (action == C4F_ACTION_LANGUAGE) {
|
||||
c4fScreen.language = (c4fScreen.language + 1) % C4F_LANGUAGE_COUNT;
|
||||
}
|
||||
#ifdef C4F_DIAG
|
||||
uint32_t pressed = wasTesting ? 0 : standardUnhandled | remoteUnhandled;
|
||||
@@ -615,13 +644,19 @@ int main(void)
|
||||
params.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
|
||||
sceUserServiceInitialize(¶ms);
|
||||
c4fScreen.running = -1; c4fScreen.busy = 1; c4fScreen.autorun = C4F_AUTORUN_UNKNOWN;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free...");
|
||||
c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free...");
|
||||
/* Read before leaving the sandbox: /app0 is only visible from inside it. */
|
||||
if (c4fAutorunLoadBundled(C4F_BUNDLED_PAYLOAD)) printf("[c4f-launcher] bundled payload unreadable\n");
|
||||
/* The sandbox refuses connections to the console itself (EACCES), so the
|
||||
* app could reach neither Control4Free nor PayLoader from inside it. */
|
||||
int sandbox = c4fSandboxLeave(), sandboxError = errno;
|
||||
if (sandbox) printf("[c4f-launcher] could not leave the sandbox, errno %d\n", sandboxError);
|
||||
if (!sandbox && c4fTextLoadSystemFonts())
|
||||
printf("[c4f-launcher] system Arabic fonts unavailable; using bundled fallback\n");
|
||||
/* The saved language lives in /data, only visible outside the sandbox. */
|
||||
int language = c4fScreen.language = c4fLanguageLoad();
|
||||
c4fLanguageUse(language);
|
||||
c4fScreen.rtl = c4fLanguageIsArabic();
|
||||
#ifdef C4F_DIAG
|
||||
c4fDiagStart(sandbox, sandboxError);
|
||||
for (int i = 0; i < C4F_DIAG_PAGES; i++) c4fScreen.diagScroll[i] = i ? C4F_DIAG_END : 0;
|
||||
@@ -631,7 +666,7 @@ int main(void)
|
||||
if (!workerStarted) {
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fScreen.locked = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize launcher. Close with PS and retry.");
|
||||
c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize launcher. Close with PS and retry.");
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
}
|
||||
c4fRenderAt = c4fLauncherTimeMs();
|
||||
@@ -640,7 +675,7 @@ int main(void)
|
||||
if (!inputStarted) {
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fScreen.locked = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize input. Close with PS and retry.");
|
||||
c4fMessageWrite(&c4fScreen.messageText, c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize input. Close with PS and retry.");
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
}
|
||||
sceSystemServiceHideSplashScreen();
|
||||
@@ -666,6 +701,15 @@ int main(void)
|
||||
#endif
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
if (done) break;
|
||||
/* L1 changed the language: the screen redraws as the state differs. */
|
||||
if (snapshot.language != language) {
|
||||
language = snapshot.language;
|
||||
c4fLanguageUse(language);
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fScreen.rtl = c4fLanguageIsArabic();
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
c4fLanguageSave(language);
|
||||
}
|
||||
snapshot.connection = c4fTesterConnectionView(&connectionStatus, connectionService, connectionCheckedAt, now, &snapshot.tester);
|
||||
int wantedRate = snapshot.tester.active ? 0 : 1;
|
||||
if (wantedRate != requestedRate) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "message.h"
|
||||
#include "language.h"
|
||||
|
||||
void c4fMessageRender(const C4fMessage *message, char *out, size_t size)
|
||||
{
|
||||
if (!out || !size) return;
|
||||
out[0] = 0;
|
||||
if (!message || !message->key) return;
|
||||
const char *key = c4fTr(message->key);
|
||||
char body[C4F_MESSAGE_RENDER_SIZE];
|
||||
switch (message->argType) {
|
||||
case C4F_MESSAGE_INT:
|
||||
snprintf(body, sizeof(body), key, message->intValue);
|
||||
break;
|
||||
case C4F_MESSAGE_UINT:
|
||||
snprintf(body, sizeof(body), key, message->uintValue);
|
||||
break;
|
||||
case C4F_MESSAGE_STRING: {
|
||||
const char *value = message->valueIsKey ? c4fTr(message->value) : message->value;
|
||||
char isolated[sizeof(message->value) * 3 + 8];
|
||||
if (c4fLanguageIsArabic() && !message->valueIsKey) {
|
||||
snprintf(isolated, sizeof(isolated), "\xE2\x81\xA8%s\xE2\x81\xA9", value);
|
||||
value = isolated;
|
||||
}
|
||||
snprintf(body, sizeof(body), key, value);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
snprintf(body, sizeof(body), "%s", key);
|
||||
break;
|
||||
}
|
||||
if (message->prefix) snprintf(out, size, c4fTr(message->prefix), body);
|
||||
else snprintf(out, size, "%s", body);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef C4F_LAUNCHER_MESSAGE_H
|
||||
#define C4F_LAUNCHER_MESSAGE_H
|
||||
/* Worker messages retain their source key and one typed argument. Their English
|
||||
* copy remains useful in logs; the renderer translates the key on every draw. */
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
|
||||
enum { C4F_MESSAGE_NONE, C4F_MESSAGE_INT, C4F_MESSAGE_UINT, C4F_MESSAGE_STRING };
|
||||
enum { C4F_MESSAGE_RENDER_SIZE = 1024 };
|
||||
typedef struct {
|
||||
const char *key, *prefix;
|
||||
int argType, intValue, valueIsKey;
|
||||
unsigned uintValue;
|
||||
char value[256];
|
||||
} C4fMessage;
|
||||
|
||||
/* This deliberately handles the launcher's existing single-argument templates,
|
||||
* rather than trying to recover a message key from already formatted text. */
|
||||
static inline void c4fMessageWrite(C4fMessage *message, char *english, size_t size,
|
||||
const char *key, ...)
|
||||
{
|
||||
memset(message, 0, sizeof(*message));
|
||||
message->key = key;
|
||||
va_list args, copy;
|
||||
va_start(args, key);
|
||||
va_copy(copy, args);
|
||||
if (english && size) vsnprintf(english, size, key, args);
|
||||
va_end(args);
|
||||
const char *conversion = strchr(key, '%');
|
||||
if (conversion) {
|
||||
++conversion;
|
||||
while (*conversion && strchr("0123456789.-+ #", *conversion)) ++conversion;
|
||||
if (*conversion == 's') {
|
||||
message->argType = C4F_MESSAGE_STRING;
|
||||
const char *value = va_arg(copy, const char *);
|
||||
snprintf(message->value, sizeof(message->value), "%s", value ? value : "");
|
||||
} else if (*conversion == 'd') {
|
||||
message->argType = C4F_MESSAGE_INT;
|
||||
message->intValue = va_arg(copy, int);
|
||||
} else if (*conversion == 'x' || *conversion == 'X' || *conversion == 'u') {
|
||||
message->argType = C4F_MESSAGE_UINT;
|
||||
message->uintValue = va_arg(copy, unsigned);
|
||||
}
|
||||
}
|
||||
va_end(copy);
|
||||
}
|
||||
|
||||
static inline void c4fMessageClear(C4fMessage *message)
|
||||
{
|
||||
memset(message, 0, sizeof(*message));
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void c4fMessageRender(const C4fMessage *message, char *out, size_t size);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -109,6 +109,7 @@ C4fLauncherAction c4fNavigate(C4fLauncherScreen *screen, uint32_t standardPresse
|
||||
screen->confirmChoice = 0;
|
||||
}
|
||||
int direction = c4fDirection(pressed);
|
||||
if (screen->rtl && (pressed & (C4F_INPUT_BUTTON_LEFT | C4F_INPUT_BUTTON_RIGHT))) direction = -direction;
|
||||
if (direction) screen->confirmChoice = direction > 0;
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
@@ -168,6 +169,11 @@ C4fLauncherAction c4fNavigate(C4fLauncherScreen *screen, uint32_t standardPresse
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
return C4F_ACTION_TESTER;
|
||||
}
|
||||
/* L1 changes the language. The diagnostic build pages with it. */
|
||||
if (!diagnostic && (standardPressed & C4F_INPUT_BUTTON_L1)) {
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
return C4F_ACTION_LANGUAGE;
|
||||
}
|
||||
if (screen->busy) return C4F_ACTION_NONE;
|
||||
if (pressed & C4F_NAV_BACK) {
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
|
||||
+72
-34
@@ -8,6 +8,7 @@
|
||||
#include <string.h>
|
||||
#include "autorun.h"
|
||||
#include "draw.h"
|
||||
#include "language.h"
|
||||
#include "logo.h"
|
||||
#include "navigation.h"
|
||||
#include "qrcodegen.h"
|
||||
@@ -25,7 +26,15 @@
|
||||
#define C4F_PANEL_TOP (C4F_ROW_TOP + C4F_ROW_HEIGHT + 28)
|
||||
#define C4F_PANEL_HEIGHT 128.0f
|
||||
|
||||
static uint32_t *c4fBackdrop;
|
||||
static uint32_t *c4fBackdrop, *c4fRtlBackdrop;
|
||||
|
||||
/* Align interface text in its allotted box; technical fields use c4fTextRaw. */
|
||||
static void c4fUiText(C4fCanvas *c, int font, float size, float x, float y,
|
||||
float width, C4fColor color, const char *text)
|
||||
{
|
||||
if (c4fTextIsRTL()) x += width - c4fTextWidth(font, size, text);
|
||||
c4fText(c, font, size, x, y, color, text);
|
||||
}
|
||||
|
||||
static float c4fClamp(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
|
||||
|
||||
@@ -417,7 +426,7 @@ static C4fLook c4fLookFor(const C4fLauncherScreen *s, char *meta, size_t metaSiz
|
||||
look = (C4fLook){ "Working", "Please wait", s->message, "Checking", C4F_WARN, C4F_ACCENT, 0 };
|
||||
} else if (s->running == 1) {
|
||||
if (s->controllers == 1) snprintf(meta, metaSize, "1 controller connected");
|
||||
else snprintf(meta, metaSize, "%d controllers connected", s->controllers);
|
||||
else snprintf(meta, metaSize, c4fTr("%d controllers connected"), s->controllers);
|
||||
look = (C4fLook){ "Running", "Control4Free is running",
|
||||
"Open the address or scan the code on your phone or PC. Pick a controller there, then "
|
||||
"choose its user on the PS4.",
|
||||
@@ -459,7 +468,7 @@ void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
c4fBandRoundRect(&c, dx, dy, dw, dh, 6 * C4F_S, 3 * C4F_S, C4F_ACCENT);
|
||||
|
||||
if (s->confirmStop) {
|
||||
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 92, C4F_TEXT, "Stop Control4Free?");
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 92, dw - 96, C4F_TEXT, "Stop Control4Free?");
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, dx + 48, dy + 148, dw - 96, 39, 2, C4F_MUTED,
|
||||
"Every controller disconnects. You can start Control4Free again from this screen.");
|
||||
if (!s->confirmMenu) {
|
||||
@@ -469,7 +478,7 @@ void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
}
|
||||
const float bw = 240, gap = 24, by = dy + 235;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
float bx = dx + dw - 48 - 2 * bw - gap + i * (bw + gap);
|
||||
float bx = dx + dw - 48 - 2 * bw - gap + (c4fTextIsRTL() ? 1 - i : i) * (bw + gap);
|
||||
int focused = s->confirmChoice == i;
|
||||
c4fFillRoundRect(&c, bx, by, bw, 64, 6 * C4F_S,
|
||||
focused ? C4F_RGBA(40, 103, 205, .85f) : C4F_TILE);
|
||||
@@ -478,13 +487,13 @@ void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
c4fTextCentered(&c, C4F_FONT_REGULAR, 30, bx + bw / 2, by + 42,
|
||||
focused ? C4F_TEXT : C4F_MUTED, i ? "Stop" : "Cancel");
|
||||
}
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 48, C4F_MUTED,
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 48, dw - 96, C4F_MUTED,
|
||||
"Arrows / D-pad: select OK / Cross: confirm Back / Circle: cancel");
|
||||
return;
|
||||
}
|
||||
|
||||
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 84, C4F_TEXT, "Actions");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 24, dx + 48, dy + 125, C4F_MUTED,
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 84, dw - 96, C4F_TEXT, "Actions");
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 24, dx + 48, dy + 125, dw - 96, C4F_MUTED,
|
||||
s->busy ? "Working... You can return to the screen." : "Use arrows or the D-pad to choose an action.");
|
||||
int focus = 0;
|
||||
for (int i = 0; i < count; i++) if ((int)actions[i] == s->actionFocus) focus = i;
|
||||
@@ -495,16 +504,17 @@ void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
focused ? C4F_RGBA(40, 103, 205, .85f) : C4F_RGBA(255, 255, 255, .04f));
|
||||
c4fInsetRoundRect(&c, rx, ry, rw, 64, 6 * C4F_S,
|
||||
focused ? 3 : 1 * C4F_S, focused ? C4F_ACCENT : C4F_LINE_SOFT);
|
||||
c4fText(&c, C4F_FONT_REGULAR, 30, rx + 24, ry + 42,
|
||||
c4fUiText(&c, C4F_FONT_REGULAR, 30, rx + 24, ry + 42, rw - 48,
|
||||
focused ? C4F_TEXT : C4F_MUTED, c4fActionLabel(s, actions[i]));
|
||||
if (focused) {
|
||||
float cx = rx + rw - 30, cy = ry + 32;
|
||||
c4fSegment(&c, cx - 5, cy - 9, cx + 4, cy, 3, C4F_TEXT);
|
||||
c4fSegment(&c, cx + 4, cy, cx - 5, cy + 9, 3, C4F_TEXT);
|
||||
float cx = c4fTextIsRTL() ? rx + 30 : rx + rw - 30, cy = ry + 32;
|
||||
float sign = c4fTextIsRTL() ? -1 : 1;
|
||||
c4fSegment(&c, cx - 5 * sign, cy - 9, cx + 4 * sign, cy, 3, C4F_TEXT);
|
||||
c4fSegment(&c, cx + 4 * sign, cy, cx - 5 * sign, cy + 9, 3, C4F_TEXT);
|
||||
}
|
||||
}
|
||||
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Select" }, { C4F_KEY_CIRCLE, "Back" } };
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 32, C4F_MUTED, "TV remote: OK select / Back return");
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 32, dw - 96, C4F_MUTED, "TV remote: OK select / Back return");
|
||||
c4fHints(&c, dx + dw - 48, dy + dh - 40, hints, 2);
|
||||
}
|
||||
|
||||
@@ -513,11 +523,29 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
|
||||
const float left = C4F_LEFT, right = C4F_RIGHT, top = C4F_ROW_TOP, height = C4F_ROW_HEIGHT;
|
||||
const float qrWidth = C4F_QR_WIDTH, mainWidth = C4F_MAIN_WIDTH, pad = 16 * C4F_S;
|
||||
char meta[48], count[8];
|
||||
const float mainX = c4fTextIsRTL() ? right - mainWidth : left;
|
||||
char meta[256], count[8], message[C4F_MESSAGE_RENDER_SIZE], autorun[C4F_MESSAGE_RENDER_SIZE];
|
||||
c4fMessageRender(&s->messageText, message, sizeof(message));
|
||||
c4fMessageRender(&s->autorunText, autorun, sizeof(autorun));
|
||||
C4fLook look = c4fLookFor(s, meta, sizeof(meta));
|
||||
if (s->busy && s->messageText.key) look.subtitle = message;
|
||||
int live = s->running == 1 && !s->locked;
|
||||
|
||||
if (c4fBackdrop) memcpy(pixels, c4fBackdrop, (size_t)c.w * (size_t)c.h * sizeof(uint32_t));
|
||||
if (c4fTextIsRTL() && !c4fRtlBackdrop) {
|
||||
c4fRtlBackdrop = malloc((size_t)c.w * c.h * sizeof(uint32_t));
|
||||
if (c4fRtlBackdrop) {
|
||||
C4fCanvas background = { c4fRtlBackdrop, c.w, c.h };
|
||||
c4fPaintBackdrop(&background, C4F_S);
|
||||
c4fBlurUnder(&background, mainX, top, mainWidth, height, 6 * C4F_S, 6 * C4F_S);
|
||||
c4fBlurUnder(&background, left, top, qrWidth, height, 6 * C4F_S, 6 * C4F_S);
|
||||
c4fBlurUnder(&background, left, C4F_PANEL_TOP, right - left, C4F_PANEL_HEIGHT, 6 * C4F_S, 10 * C4F_S);
|
||||
#ifndef C4F_DIAG
|
||||
c4fDrawSolidarityBadge(&background, C4F_SCREEN_WIDTH - 64, 40);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
const uint32_t *background = c4fTextIsRTL() && c4fRtlBackdrop ? c4fRtlBackdrop : c4fBackdrop;
|
||||
if (background) memcpy(pixels, background, (size_t)c.w * (size_t)c.h * sizeof(uint32_t));
|
||||
else memset(pixels, 0, (size_t)c.w * (size_t)c.h * sizeof(uint32_t));
|
||||
|
||||
/* .topbar: the app mark, the name and the state. */
|
||||
@@ -528,25 +556,27 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
c4fText(&c, C4F_FONT_LIGHT, 13 * C4F_S, x + 16 * C4F_S, 62, C4F_MUTED, look.pill);
|
||||
|
||||
/* .screen-title and .screen-sub */
|
||||
c4fText(&c, C4F_FONT_LIGHT, 40 * C4F_S, left, 196, C4F_TEXT, look.title);
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, left, 250, 1500, 39, 2, C4F_MUTED, look.subtitle);
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 40 * C4F_S, left, 196, right - left, C4F_TEXT, look.title);
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, left, 250, right - left, 39, 2, C4F_MUTED, look.subtitle);
|
||||
|
||||
/* Where to connect, as a controller tile. */
|
||||
c4fTile(&c, left, top, mainWidth, height, look.bar);
|
||||
c4fTile(&c, mainX, top, mainWidth, height, look.bar);
|
||||
if (s->running == 1) snprintf(count, sizeof(count), "%d", s->controllers);
|
||||
else snprintf(count, sizeof(count), "-");
|
||||
c4fRingBadge(&c, left + pad + 26 * C4F_S, top + pad + 26 * C4F_S + 4, look.bar, count);
|
||||
float textX = left + pad + 52 * C4F_S + 22;
|
||||
c4fText(&c, C4F_FONT_REGULAR, 30, textX, top + 66, C4F_TEXT, "Open on your phone or PC");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, textX, top + 102, C4F_MUTED, look.meta);
|
||||
c4fRingBadge(&c, c4fTextIsRTL() ? mainX + mainWidth - pad - 26 * C4F_S : mainX + pad + 26 * C4F_S,
|
||||
top + pad + 26 * C4F_S + 4, look.bar, count);
|
||||
float textX = mainX + pad + (c4fTextIsRTL() ? 0 : 52 * C4F_S + 22);
|
||||
float textW = mainWidth - 2 * pad - 52 * C4F_S - 22;
|
||||
c4fUiText(&c, C4F_FONT_REGULAR, 30, textX, top + 66, textW, C4F_TEXT, "Open on your phone or PC");
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 23, textX, top + 102, textW, C4F_MUTED, look.meta);
|
||||
|
||||
float fieldX = left + pad, fieldY = top + 150, fieldWidth = mainWidth - 2 * pad, fieldHeight = 88;
|
||||
float fieldX = mainX + pad, fieldY = top + 150, fieldWidth = mainWidth - 2 * pad, fieldHeight = 88;
|
||||
c4fFillRoundRect(&c, fieldX, fieldY, fieldWidth, fieldHeight, 4 * C4F_S, C4F_RGBA(0, 0, 0, .3f));
|
||||
c4fInsetRoundRect(&c, fieldX, fieldY, fieldWidth, fieldHeight, 4 * C4F_S, 1 * C4F_S, C4F_LINE);
|
||||
if (s->address[0])
|
||||
c4fText(&c, C4F_FONT_REGULAR, 40, fieldX + 28, fieldY + fieldHeight / 2 + 14, live ? C4F_TEXT : C4F_MUTED, s->address);
|
||||
c4fTextRaw(&c, C4F_FONT_REGULAR, 40, fieldX + 28, fieldY + fieldHeight / 2 + 14, live ? C4F_TEXT : C4F_MUTED, s->address);
|
||||
else
|
||||
c4fText(&c, C4F_FONT_LIGHT, 28, fieldX + 28, fieldY + fieldHeight / 2 + 10, C4F_MUTED,
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 28, fieldX + 28, fieldY + fieldHeight / 2 + 10, fieldWidth - 56, C4F_MUTED,
|
||||
"Connect the PS4 to your home network");
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, fieldX, fieldY + fieldHeight + 44, fieldWidth, 36, 2, C4F_MUTED,
|
||||
"Use the same network as the PS4. Touch, a keyboard and gamepads connected to the phone "
|
||||
@@ -555,12 +585,12 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
/* The last result along the bottom of the tile. */
|
||||
float lineY = top + height - 104;
|
||||
c4fFillRoundRect(&c, fieldX, lineY, fieldWidth, 1 * C4F_S, 0, C4F_LINE_SOFT);
|
||||
c4fDot(&c, fieldX + 4 * C4F_S, lineY + 44, look.dot, look.glow);
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, fieldX + 24, lineY + 52, fieldWidth - 24, 34, 2, C4F_TEXT,
|
||||
s->busy ? "Working..." : s->message);
|
||||
c4fDot(&c, c4fTextIsRTL() ? fieldX + fieldWidth - 4 * C4F_S : fieldX + 4 * C4F_S, lineY + 44, look.dot, look.glow);
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, fieldX + (c4fTextIsRTL() ? 0 : 24), lineY + 52, fieldWidth - 24, 34, 2, C4F_TEXT,
|
||||
s->busy ? "Working..." : s->messageText.key ? message : s->message);
|
||||
|
||||
/* The QR code tile. */
|
||||
float qrX = C4F_QR_LEFT, card = 340, cardX = qrX + (qrWidth - card) / 2, cardY = top + 34;
|
||||
float qrX = c4fTextIsRTL() ? left : C4F_QR_LEFT, card = 340, cardX = qrX + (qrWidth - card) / 2, cardY = top + 34;
|
||||
c4fTile(&c, qrX, top, qrWidth, height, look.bar);
|
||||
if (live && s->address[0]) {
|
||||
c4fQrCard(&c, cardX, cardY, card, s->address);
|
||||
@@ -577,10 +607,13 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
/* .panel: whether GoldHEN starts it by itself. */
|
||||
const char *panelTitle, *panelText;
|
||||
C4fColor panelDot = C4F_FAINT;
|
||||
char versions[160];
|
||||
char versions[256];
|
||||
int mismatch = live && s->runningVersion[0] && strcmp(s->runningVersion, C4F_LAUNCHER_VERSION);
|
||||
if (mismatch) {
|
||||
snprintf(versions, sizeof(versions), "Installed %s; running %s", C4F_LAUNCHER_VERSION, s->runningVersion);
|
||||
char installed[64], running[64];
|
||||
snprintf(installed, sizeof(installed), c4fTextIsRTL() ? "\xE2\x81\xA8%s\xE2\x81\xA9" : "%s", C4F_LAUNCHER_VERSION);
|
||||
snprintf(running, sizeof(running), c4fTextIsRTL() ? "\xE2\x81\xA8%s\xE2\x81\xA9" : "%s", s->runningVersion);
|
||||
snprintf(versions, sizeof(versions), c4fTr("Installed %s; running %s"), installed, running);
|
||||
panelTitle = versions;
|
||||
panelText = "Press Square, then Cross to stop, and Cross again to start this app's version. All players disconnect.";
|
||||
panelDot = C4F_WARN;
|
||||
@@ -598,20 +631,25 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
panelText = "Press Triangle to turn auto-start on. GoldHEN then starts Control4Free each time it loads.";
|
||||
} else {
|
||||
panelTitle = "Auto-start status unknown";
|
||||
panelText = s->autorunNote[0] ? s->autorunNote : "Checking with GoldHEN...";
|
||||
panelText = s->autorunText.key ? autorun : s->autorunNote[0] ? s->autorunNote : "Checking with GoldHEN...";
|
||||
panelDot = s->autorunNote[0] ? C4F_BAD : C4F_WARN;
|
||||
}
|
||||
float panelY = C4F_PANEL_TOP;
|
||||
c4fPanel(&c, left, panelY, right - left, C4F_PANEL_HEIGHT, C4F_PANEL, C4F_LINE_SOFT);
|
||||
c4fDot(&c, left + 20 * C4F_S + 4 * C4F_S, panelY + 43, panelDot, s->autorun == C4F_AUTORUN_ON);
|
||||
c4fText(&c, C4F_FONT_LIGHT, 19 * C4F_S, left + 20 * C4F_S + 24, panelY + 52, C4F_TEXT, panelTitle);
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 19 * C4F_S, left + 20 * C4F_S + 24, panelY + 52,
|
||||
right - left - 40 * C4F_S - 24, C4F_TEXT, panelTitle);
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 23, left + 20 * C4F_S, panelY + 94, right - left - 40 * C4F_S, 34, 1, C4F_MUTED,
|
||||
panelText);
|
||||
|
||||
/* .home-foot and the button guide. */
|
||||
if (!s->confirmStop && !s->actionMenu)
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, left, 973, C4F_MUTED, "TV remote: OK opens actions / arrows select / Back returns");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 20, left, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
|
||||
c4fUiText(&c, C4F_FONT_LIGHT, 23, left, 973, right - left, C4F_MUTED, "TV remote: OK opens actions / arrows select / Back returns");
|
||||
float footX = left + c4fText(&c, C4F_FONT_LIGHT, 20, left, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
|
||||
static const char *const languages[C4F_LANGUAGE_COUNT] = {
|
||||
"L1 Language: automatic", "L1 Language: English", "L1 Language: Portuguese (Brazil)", "L1 Language: Arabic" };
|
||||
if (!s->confirmStop && !s->actionMenu && s->language >= 0 && s->language < C4F_LANGUAGE_COUNT)
|
||||
c4fText(&c, C4F_FONT_LIGHT, 20, footX + 36, 1030, C4F_FAINT, languages[s->language]);
|
||||
if (!s->confirmStop && !s->busy) {
|
||||
C4fHint hints[5];
|
||||
int n = 0;
|
||||
|
||||
+7
-1
@@ -16,8 +16,12 @@ typedef enum {
|
||||
C4F_ACTION_STOP,
|
||||
C4F_ACTION_CLOSE,
|
||||
C4F_ACTION_CAPTURE,
|
||||
C4F_ACTION_TESTER
|
||||
C4F_ACTION_TESTER,
|
||||
C4F_ACTION_LANGUAGE
|
||||
} C4fLauncherAction;
|
||||
/* The app's language, cycled with L1 (language.h). */
|
||||
enum { C4F_LANGUAGE_AUTO, C4F_LANGUAGE_ENGLISH, C4F_LANGUAGE_PORTUGUESE,
|
||||
C4F_LANGUAGE_ARABIC, C4F_LANGUAGE_COUNT };
|
||||
/* Connection round trips for the selected user's virtual-controller slots.
|
||||
* A negative RTT means that no fresh measurement is available yet. */
|
||||
typedef struct {
|
||||
@@ -30,7 +34,9 @@ typedef struct {
|
||||
int actionMenu, actionFocus;
|
||||
int confirmChoice, confirmMenu; /* 0 Cancel / 1 Stop; menu-origin confirmation. */
|
||||
int autorun; /* C4F_AUTORUN_* from autorun.h */
|
||||
int language, rtl; /* C4F_LANGUAGE_*; resolved direction for input sampling. */
|
||||
char address[64], message[160], autorunNote[96], runningVersion[32], inputNote[160];
|
||||
C4fMessage messageText, autorunText;
|
||||
C4fTesterView tester; /* the controller test, when active */
|
||||
C4fTesterConnection connection;
|
||||
#ifdef C4F_DIAG
|
||||
|
||||
+27
-18
@@ -34,7 +34,8 @@
|
||||
|
||||
static char c4fHost[16];
|
||||
static char c4fProblem[128];
|
||||
static int c4fReached;
|
||||
static int c4fReached, c4fProblemIsKey;
|
||||
static C4fMessage c4fServiceMessage;
|
||||
|
||||
uint64_t c4fLauncherTimeMs(void)
|
||||
{
|
||||
@@ -53,6 +54,13 @@ void c4fLauncherSetHost(const char *ipv4)
|
||||
}
|
||||
|
||||
const char *c4fLauncherProblem(void) { return c4fProblem; }
|
||||
int c4fLauncherProblemIsKey(void) { return c4fProblemIsKey; }
|
||||
const C4fMessage *c4fLauncherMessage(void)
|
||||
{
|
||||
if (c4fServiceMessage.argType == C4F_MESSAGE_STRING)
|
||||
c4fServiceMessage.valueIsKey = c4fProblemIsKey;
|
||||
return &c4fServiceMessage;
|
||||
}
|
||||
|
||||
static void c4fNote(const char *address, const char *what)
|
||||
{
|
||||
@@ -168,7 +176,7 @@ static int c4fHttp(const char *method, const char *path, char *body, size_t capa
|
||||
{
|
||||
const char *targets[2];
|
||||
int count = c4fTargets(targets), refused = 0, connected = 0;
|
||||
c4fProblem[0] = 0;
|
||||
c4fProblem[0] = 0; c4fProblemIsKey = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int fd = c4fConnect(targets[i], C4F_SERVICE_PORT);
|
||||
if (fd < 0) {
|
||||
@@ -218,6 +226,7 @@ int c4fLauncherProbe(C4fServiceStatus *status)
|
||||
jsmn_init(&parser);
|
||||
int count = jsmn_parse(&parser, body, strlen(body), tokens, 64);
|
||||
snprintf(c4fProblem, sizeof(c4fProblem), "the status reply is not from Control4Free");
|
||||
c4fProblemIsKey = 1;
|
||||
if (count < 1 || tokens[0].type != JSMN_OBJECT || tokens[0].end != (int)strlen(body)) return -1;
|
||||
int app = 0, api = 0, controllers = 0, stopping = 0;
|
||||
unsigned latencyFields[C4F_SERVICE_PADS] = { 0 };
|
||||
@@ -258,7 +267,7 @@ int c4fLauncherProbe(C4fServiceStatus *status)
|
||||
}
|
||||
if (!(app && api && controllers && stopping)) return -1;
|
||||
for (int p = 0; p < C4F_SERVICE_PADS; p++) if (latencyFields[p] != 7) status->latencyAvailable = 0;
|
||||
c4fProblem[0] = 0;
|
||||
c4fProblem[0] = 0; c4fProblemIsKey = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -266,29 +275,29 @@ int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *mes
|
||||
{
|
||||
C4fServiceStatus status;
|
||||
int probe = c4fLauncherProbe(&status);
|
||||
if (probe == 1) { snprintf(message, size, "Already running. Open the address on your phone or PC."); return 0; }
|
||||
if (probe != 0 && !c4fReached) { snprintf(message, size, "The app cannot check whether Control4Free is running (%s).", c4fProblem); return -1; }
|
||||
if (probe != 0) { snprintf(message, size, "Control4Free is not responding (%s). Wait a moment after waking. If it stays stuck, restart the PS4.", c4fProblem); return -1; }
|
||||
if (!payload) { snprintf(message, size, "The payload is missing. Reinstall the Control4Free package."); return -1; }
|
||||
if (probe == 1) { c4fMessageWrite(&c4fServiceMessage, message, size, "Already running. Open the address on your phone or PC."); return 0; }
|
||||
if (probe != 0 && !c4fReached) { c4fMessageWrite(&c4fServiceMessage, message, size, "The app cannot check whether Control4Free is running (%s).", c4fProblem); return -1; }
|
||||
if (probe != 0) { c4fMessageWrite(&c4fServiceMessage, message, size, "Control4Free is not responding (%s). Wait a moment after waking. If it stays stuck, restart the PS4.", c4fProblem); return -1; }
|
||||
if (!payload) { c4fMessageWrite(&c4fServiceMessage, message, size, "The payload is missing. Reinstall the Control4Free package."); return -1; }
|
||||
if (payloadSize < 64 || memcmp(payload, "\177ELF", 4) || payload[4] != 2 || payload[5] != 1) {
|
||||
snprintf(message, size, "The bundled payload is damaged. Reinstall the Control4Free package."); return -1;
|
||||
c4fMessageWrite(&c4fServiceMessage, message, size, "The bundled payload is damaged. Reinstall the Control4Free package."); return -1;
|
||||
}
|
||||
const char *targets[2];
|
||||
int count = c4fTargets(targets), fd = -1;
|
||||
for (int i = 0; i < count && fd < 0; i++) fd = c4fConnect(targets[i], C4F_PAYLOADER_PORT);
|
||||
if (fd < 0) { snprintf(message, size, "PayLoader did not answer (errno %d). Enable PayLoader Server in GoldHEN > Server Settings, then retry Start.", errno); return -1; }
|
||||
if (fd < 0) { c4fMessageWrite(&c4fServiceMessage, message, size, "PayLoader did not answer (errno %d). Enable PayLoader Server in GoldHEN > Server Settings, then retry Start.", errno); return -1; }
|
||||
int failed = c4fSendAll(fd, payload, payloadSize, c4fLauncherTimeMs() + 10000);
|
||||
shutdown(fd, SHUT_WR); close(fd);
|
||||
if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Retry Start."); return -2; }
|
||||
if (failed) { c4fMessageWrite(&c4fServiceMessage, message, size, "The transfer to PayLoader broke off. Retry Start."); return -2; }
|
||||
uint64_t deadline;
|
||||
deadline = c4fLauncherTimeMs() + 20000;
|
||||
while (c4fLauncherTimeMs() < deadline) {
|
||||
if (c4fLauncherProbe(&status) == 1 && !status.stopping) {
|
||||
snprintf(message, size, "Ready. Open the address on your phone or PC."); return 0;
|
||||
c4fMessageWrite(&c4fServiceMessage, message, size, "Ready. Open the address on your phone or PC."); return 0;
|
||||
}
|
||||
usleep(250000);
|
||||
}
|
||||
snprintf(message, size, "Sent, but Control4Free did not start (%s). Retry Start.", c4fProblem);
|
||||
c4fMessageWrite(&c4fServiceMessage, message, size, "Sent, but Control4Free did not start (%s). Retry Start.", c4fProblem);
|
||||
/* Sending again is safe: a copy that did start holds the payload's
|
||||
* instance lock, so a second one quits at once. */
|
||||
return -2;
|
||||
@@ -298,16 +307,16 @@ int c4fLauncherStop(char *message, size_t size)
|
||||
{
|
||||
C4fServiceStatus status;
|
||||
int probe = c4fLauncherProbe(&status);
|
||||
if (probe == 0) { snprintf(message, size, "Stopped. Ready to start again."); return 0; }
|
||||
if (probe != 1 && !c4fReached) { snprintf(message, size, "The app cannot reach Control4Free (%s). Stop it on its controller page.", c4fProblem); return -1; }
|
||||
if (probe != 1) { snprintf(message, size, "This copy does not answer the app (%s). Stop it on its controller page.", c4fProblem); return -1; }
|
||||
if (probe == 0) { c4fMessageWrite(&c4fServiceMessage, message, size, "Stopped. Ready to start again."); return 0; }
|
||||
if (probe != 1 && !c4fReached) { c4fMessageWrite(&c4fServiceMessage, message, size, "The app cannot reach Control4Free (%s). Stop it on its controller page.", c4fProblem); return -1; }
|
||||
if (probe != 1) { c4fMessageWrite(&c4fServiceMessage, message, size, "This copy does not answer the app (%s). Stop it on its controller page.", c4fProblem); return -1; }
|
||||
char reply[256];
|
||||
int ret = c4fHttp("POST", "/api/stop", reply, sizeof(reply));
|
||||
if (ret != 1) { snprintf(message, size, "Stop was not confirmed (%s). Check the controller page.", c4fProblem); return -1; }
|
||||
if (ret != 1) { c4fMessageWrite(&c4fServiceMessage, message, size, "Stop was not confirmed (%s). Check the controller page.", c4fProblem); return -1; }
|
||||
uint64_t deadline = c4fLauncherTimeMs() + 5000;
|
||||
while (c4fLauncherTimeMs() < deadline) {
|
||||
if (c4fLauncherProbe(&status) == 0) { snprintf(message, size, "Stopped. Ready to start again."); return 0; }
|
||||
if (c4fLauncherProbe(&status) == 0) { c4fMessageWrite(&c4fServiceMessage, message, size, "Stopped. Ready to start again."); return 0; }
|
||||
usleep(100000);
|
||||
}
|
||||
snprintf(message, size, "Still stopping. Wait a moment, then try again."); return -1;
|
||||
c4fMessageWrite(&c4fServiceMessage, message, size, "Still stopping. Wait a moment, then try again."); return -1;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#define C4F_LAUNCHER_SERVICE_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "message.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -26,6 +27,8 @@ uint64_t c4fLauncherTimeMs(void);
|
||||
void c4fLauncherSetHost(const char *ipv4);
|
||||
/* Why the last probe could not confirm the service, for the screen. */
|
||||
const char *c4fLauncherProblem(void);
|
||||
int c4fLauncherProblemIsKey(void);
|
||||
const C4fMessage *c4fLauncherMessage(void);
|
||||
/* DOWN only when nothing accepted a connection and something refused one.
|
||||
* Ambiguous answers must not allow starting another copy in the shared
|
||||
* payload host. */
|
||||
|
||||
+19
-14
@@ -122,9 +122,10 @@ static void c4fTesterUsers(C4fTester *t, uint64_t now)
|
||||
r->name[0] = 0;
|
||||
if (t->ops.userName) t->ops.userName(t->ops.context, r->user, r->name, (int)sizeof(r->name));
|
||||
r->name[sizeof(r->name) - 1] = 0;
|
||||
/* Names are drawn in plain ASCII. */
|
||||
for (char *p = r->name; *p; p++) if ((unsigned char)*p < 32 || (unsigned char)*p > 126) *p = '?';
|
||||
if (!r->name[0]) snprintf(r->name, sizeof(r->name), "User %d", s + 1);
|
||||
/* Preserve UTF-8 names. Only replace ASCII control characters. */
|
||||
for (char *p = r->name; *p; p++) if ((unsigned char)*p < 32 || (unsigned char)*p == 127) *p = '?';
|
||||
r->nameGenerated = !r->name[0];
|
||||
if (r->nameGenerated) snprintf(r->name, sizeof(r->name), "User %d", s + 1);
|
||||
r->retryAt = now;
|
||||
c4fReaderState(t, s, C4F_TESTER_OPENING, 0);
|
||||
known = 1;
|
||||
@@ -204,13 +205,13 @@ static void c4fTesterNextUser(C4fTester *t, int direction)
|
||||
}
|
||||
}
|
||||
c4fTesterForgetSignedOut(t);
|
||||
t->note[0] = 0;
|
||||
t->note[0] = 0; c4fMessageClear(&t->noteText);
|
||||
}
|
||||
|
||||
static void c4fTesterResult(C4fTester *t, int result, const char *done)
|
||||
{
|
||||
if (result) snprintf(t->note, sizeof(t->note), "The PS4 refused that (0x%08x).", (unsigned)result);
|
||||
else snprintf(t->note, sizeof(t->note), "%s", done);
|
||||
if (result) c4fMessageWrite(&t->noteText, t->note, sizeof(t->note), "The PS4 refused that (0x%08x).", (unsigned)result);
|
||||
else c4fMessageWrite(&t->noteText, t->note, sizeof(t->note), done);
|
||||
}
|
||||
|
||||
static void c4fTesterAct(C4fTester *t, int item, uint64_t now)
|
||||
@@ -218,7 +219,7 @@ static void c4fTesterAct(C4fTester *t, int item, uint64_t now)
|
||||
C4fTesterView view;
|
||||
c4fTesterView(t, &view);
|
||||
if (!c4fTesterItemAvailable(&view, item)) {
|
||||
snprintf(t->note, sizeof(t->note), "%s", item == C4F_TESTER_USER ?
|
||||
c4fMessageWrite(&t->noteText, t->note, sizeof(t->note), item == C4F_TESTER_USER ?
|
||||
"No other user is signed in." : "This user has no controller to test.");
|
||||
return;
|
||||
}
|
||||
@@ -232,7 +233,7 @@ static void c4fTesterAct(C4fTester *t, int item, uint64_t now)
|
||||
return;
|
||||
}
|
||||
if (!r || r->user < 0 || r->handle < 0 || r->state != C4F_TESTER_READY) {
|
||||
snprintf(t->note, sizeof(t->note), "This user has no controller to test.");
|
||||
c4fMessageWrite(&t->noteText, t->note, sizeof(t->note), "This user has no controller to test.");
|
||||
return;
|
||||
}
|
||||
switch (item) {
|
||||
@@ -257,7 +258,7 @@ static void c4fTesterAct(C4fTester *t, int item, uint64_t now)
|
||||
r->lightSet = 1;
|
||||
memcpy(r->light, rgb, 3);
|
||||
t->colorMenu = 1;
|
||||
t->note[0] = 0;
|
||||
t->note[0] = 0; c4fMessageClear(&t->noteText);
|
||||
} else {
|
||||
c4fTesterResult(t, result, "");
|
||||
}
|
||||
@@ -294,7 +295,9 @@ void c4fTesterOpen(C4fTester *t, uint64_t now)
|
||||
{
|
||||
if (t->active) return;
|
||||
C4fTesterOps ops = t->ops;
|
||||
int rtl = t->rtl;
|
||||
c4fTesterInit(t, &ops);
|
||||
t->rtl = rtl;
|
||||
t->active = 1;
|
||||
c4fTesterUsers(t, now);
|
||||
}
|
||||
@@ -386,9 +389,9 @@ static void c4fTesterMenuMove(C4fTester *t, uint32_t direction)
|
||||
} else if (direction & C4F_INPUT_BUTTON_DOWN) {
|
||||
row = (row + 1) % C4F_TESTER_MENU_ROWS;
|
||||
} else if (direction & C4F_INPUT_BUTTON_LEFT) {
|
||||
column = (column + C4F_TESTER_MENU_COLUMNS - 1) % C4F_TESTER_MENU_COLUMNS;
|
||||
column = (column + (t->rtl ? 1 : C4F_TESTER_MENU_COLUMNS - 1)) % C4F_TESTER_MENU_COLUMNS;
|
||||
} else if (direction & C4F_INPUT_BUTTON_RIGHT) {
|
||||
column = (column + 1) % C4F_TESTER_MENU_COLUMNS;
|
||||
column = (column + (t->rtl ? C4F_TESTER_MENU_COLUMNS - 1 : 1)) % C4F_TESTER_MENU_COLUMNS;
|
||||
}
|
||||
t->focus = row * C4F_TESTER_MENU_COLUMNS + column;
|
||||
}
|
||||
@@ -397,7 +400,7 @@ static void c4fTesterColorInput(C4fTester *t, uint32_t navigation, uint32_t pres
|
||||
{
|
||||
if (pressed & C4F_INPUT_BUTTON_CIRCLE) {
|
||||
t->colorMenu = 0;
|
||||
snprintf(t->note, sizeof(t->note), "Light bar set. It goes back to normal when you leave.");
|
||||
c4fMessageWrite(&t->noteText, t->note, sizeof(t->note), "Light bar set. It goes back to normal when you leave.");
|
||||
return;
|
||||
}
|
||||
if (navigation & C4F_INPUT_BUTTON_UP) t->colorChannel = (t->colorChannel + 2) % 3;
|
||||
@@ -416,7 +419,7 @@ static void c4fTesterColorInput(C4fTester *t, uint32_t navigation, uint32_t pres
|
||||
if (!result) {
|
||||
r->lightSet = 1;
|
||||
memcpy(r->light, rgb, sizeof(rgb));
|
||||
t->note[0] = 0;
|
||||
t->note[0] = 0; c4fMessageClear(&t->noteText);
|
||||
} else c4fTesterResult(t, result, "");
|
||||
} else if (pressed & C4F_INPUT_BUTTON_CROSS) t->colorFine = !t->colorFine;
|
||||
}
|
||||
@@ -435,7 +438,7 @@ int c4fTesterFrame(C4fTester *t, uint64_t now, uint32_t remotePressed)
|
||||
if (t->rumble && now >= t->rumbleUntil) c4fTesterStopRumble(t);
|
||||
if (t->colorMenu && (t->selected < 0 || t->readers[t->selected].state != C4F_TESTER_READY)) {
|
||||
t->colorMenu = 0;
|
||||
snprintf(t->note, sizeof(t->note), "This user's controller is unavailable.");
|
||||
c4fMessageWrite(&t->noteText, t->note, sizeof(t->note), "This user's controller is unavailable.");
|
||||
}
|
||||
if (menuOpener >= 0 && !t->menu) {
|
||||
c4fTesterMenuOpen(t, menuOpener);
|
||||
@@ -520,10 +523,12 @@ void c4fTesterView(const C4fTester *t, C4fTesterView *v)
|
||||
if (!v->user[s].used) continue;
|
||||
v->user[s].id = r->user;
|
||||
snprintf(v->user[s].name, sizeof(v->user[s].name), "%s", r->name);
|
||||
v->user[s].nameGenerated = r->nameGenerated;
|
||||
v->user[s].state = r->state;
|
||||
v->user[s].error = r->error;
|
||||
}
|
||||
snprintf(v->note, sizeof(v->note), "%s", t->note);
|
||||
v->noteText = t->noteText;
|
||||
if (t->selected < 0) return;
|
||||
const C4fTesterReader *r = &t->readers[t->selected];
|
||||
v->rumble = t->rumble && t->rumbleReader == t->selected ? t->rumble : 0;
|
||||
|
||||
+7
-4
@@ -4,12 +4,13 @@
|
||||
* game reads it, with rumble and light-bar test actions. The native pad and
|
||||
* user calls are injected, so this logic also runs in host tests. */
|
||||
#include <stdint.h>
|
||||
#include "message.h"
|
||||
|
||||
#define C4F_TESTER_USERS 4
|
||||
#define C4F_TESTER_HOLD_MS 2000
|
||||
#define C4F_TESTER_RUMBLE_MS 1000
|
||||
#define C4F_TESTER_RETRY_MS 1000
|
||||
#define C4F_TESTER_NAME 24
|
||||
#define C4F_TESTER_NAME 72 /* Room for a complete 16-character UTF-8 username. */
|
||||
#define C4F_TESTER_MENU_COLUMNS 4
|
||||
#define C4F_TESTER_REPEAT_DELAY_MS 350
|
||||
#define C4F_TESTER_REPEAT_MS 100
|
||||
@@ -65,7 +66,7 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
int32_t user; /* -1: empty slot */
|
||||
int handle, owned, state, lightSet, seen;
|
||||
int handle, owned, state, lightSet, seen, nameGenerated;
|
||||
uint32_t error, previous, pressed, menuBlocked, repeatDirection;
|
||||
uint64_t retryAt, repeatAt;
|
||||
C4fTesterHold hold;
|
||||
@@ -76,12 +77,13 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
C4fTesterOps ops;
|
||||
int active, menu, focus, armed, selected, rumble, rumbleReader;
|
||||
int active, menu, focus, armed, selected, rumble, rumbleReader, rtl;
|
||||
int colorMenu, colorChannel, colorFine;
|
||||
unsigned menuOpeners;
|
||||
uint64_t usersAt, rumbleUntil;
|
||||
C4fTesterReader readers[C4F_TESTER_USERS];
|
||||
char note[96];
|
||||
C4fMessage noteText;
|
||||
} C4fTester;
|
||||
|
||||
/* What the screen shows, in whole display units so that an unchanged reading
|
||||
@@ -90,7 +92,7 @@ typedef struct {
|
||||
int active, menu, focus, selected, rumble, lightSet;
|
||||
int colorMenu, colorChannel, colorFine;
|
||||
uint8_t light[3];
|
||||
struct { int32_t id; char name[C4F_TESTER_NAME]; int state, used; uint32_t error; } user[C4F_TESTER_USERS];
|
||||
struct { int32_t id; char name[C4F_TESTER_NAME]; int state, used, nameGenerated; uint32_t error; } user[C4F_TESTER_USERS];
|
||||
uint32_t buttons;
|
||||
uint8_t lx, ly, rx, ry, l2, r2, touches;
|
||||
uint16_t touchX[2], touchY[2];
|
||||
@@ -98,6 +100,7 @@ typedef struct {
|
||||
int16_t accel[3]; /* x 100, G */
|
||||
int16_t gyro[3]; /* x 100, rad/s */
|
||||
char note[96];
|
||||
C4fMessage noteText;
|
||||
} C4fTesterView;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+127
-48
@@ -8,6 +8,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "input.h"
|
||||
#include "language.h"
|
||||
#include "style.h"
|
||||
|
||||
/* Layout, in TV pixels. */
|
||||
@@ -57,12 +58,32 @@ static const float c4fLayout[C4F_C_COUNT][4] = {
|
||||
#define C4F_KNOB_TRAVEL 0.27f
|
||||
|
||||
typedef struct { float x, y, w, h; } C4fBox;
|
||||
|
||||
static void c4fTesterText(C4fCanvas *c, int font, float size, float x, float y,
|
||||
float width, C4fColor color, const char *text)
|
||||
{
|
||||
float measured = c4fTextWidth(font, size, text);
|
||||
if (measured > width && width > 0) {
|
||||
size = floorf(size * width / measured);
|
||||
measured = c4fTextWidth(font, size, text);
|
||||
}
|
||||
if (c4fTextIsRTL()) x += width - measured;
|
||||
c4fText(c, font, size, x, y, color, text);
|
||||
}
|
||||
|
||||
static const char *c4fTesterUserName(const C4fTesterView *view, int user, char *buffer, size_t size)
|
||||
{
|
||||
if (!view->user[user].nameGenerated) return view->user[user].name;
|
||||
snprintf(buffer, size, c4fTr("User %d"), user + 1);
|
||||
return buffer;
|
||||
}
|
||||
typedef struct { float at; C4fColor color; } C4fStop;
|
||||
|
||||
static const C4fPlacement c4fIdentity = { 0, 0, 1, 0, 0, 0 };
|
||||
static uint32_t *c4fLayer;
|
||||
static uint32_t *c4fToolsBase, *c4fToolsFrame;
|
||||
static int c4fToolsBaseReady;
|
||||
static unsigned c4fLayersTranslation; /* the text translation the still layers were drawn in */
|
||||
|
||||
typedef struct { uint32_t *px; int x, y, w, h; } C4fSprite;
|
||||
static C4fSprite c4fKnobs[3], c4fPressed[16], c4fTouchGlow, c4fLightStrip;
|
||||
@@ -651,7 +672,7 @@ static void c4fBuildLayer(void)
|
||||
c4fText(&c, C4F_FONT_LIGHT, 13 * C4F_S, x + 16 * C4F_S, 62, C4F_MUTED, "Controller test");
|
||||
c4fDrawSolidarityBadge(&c, C4F_SCREEN_WIDTH - 64, 40);
|
||||
|
||||
c4fText(&c, C4F_FONT_LIGHT, 48, C4F_T_LEFT, 140, C4F_TEXT, "Controller test");
|
||||
c4fTesterText(&c, C4F_FONT_LIGHT, 48, C4F_T_LEFT, 140, C4F_T_RIGHT - C4F_T_LEFT, C4F_TEXT, "Controller test");
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 184, C4F_T_RIGHT - C4F_T_LEFT, 34, 2, C4F_MUTED,
|
||||
"See the controller input received by this PS4. Select a user, then press buttons or move "
|
||||
"the sticks. PS and Share go to the PS4 itself and aren't shown here.");
|
||||
@@ -662,25 +683,25 @@ static void c4fBuildLayer(void)
|
||||
|
||||
/* The panel's headings and labels. */
|
||||
const float tx = C4F_T_TEXT_X;
|
||||
c4fText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_STICKS, C4F_MUTED, "Sticks");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 26, tx, C4F_T_ROW_STICKS + 40, C4F_TEXT, "Left");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 26, tx, C4F_T_ROW_STICKS + 76, C4F_TEXT, "Right");
|
||||
c4fText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_TRIGGERS, C4F_MUTED, "Triggers");
|
||||
c4fTesterText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_STICKS, C4F_T_INFO_W - 72, C4F_MUTED, "Sticks");
|
||||
c4fTesterText(&c, C4F_FONT_LIGHT, 26, tx, C4F_T_ROW_STICKS + 40, 108, C4F_TEXT, "Left");
|
||||
c4fTesterText(&c, C4F_FONT_LIGHT, 26, tx, C4F_T_ROW_STICKS + 76, 108, C4F_TEXT, "Right");
|
||||
c4fTesterText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_TRIGGERS, C4F_T_INFO_W - 72, C4F_MUTED, "Triggers");
|
||||
for (int i = 0; i < 2; i++) {
|
||||
float y = C4F_T_ROW_TRIGGERS + 40 + i * 36;
|
||||
c4fText(&c, C4F_FONT_LIGHT, 26, tx, y, C4F_TEXT, i ? "R2" : "L2");
|
||||
c4fFillRoundRect(&c, C4F_T_BAR_X, y - 14, C4F_T_BAR_W, 14, 7, C4F_RGBA(0, 0, 0, .35f));
|
||||
}
|
||||
c4fText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_TOUCH, C4F_MUTED, "Touchpad");
|
||||
c4fText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_MOTION, C4F_MUTED, "Motion");
|
||||
c4fTesterText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_TOUCH, 128, C4F_MUTED, "Touchpad");
|
||||
c4fTesterText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_MOTION, 224, C4F_MUTED, "Motion");
|
||||
static const char *columns[4] = { "x", "y", "z", "w" };
|
||||
for (int i = 0; i < 4; i++)
|
||||
c4fTextRight(&c, C4F_FONT_REGULAR, 22, C4F_T_COLUMN_X + (i + 1) * C4F_T_COLUMN_W, C4F_T_ROW_MOTION,
|
||||
C4F_FAINT, columns[i]);
|
||||
c4fText(&c, C4F_FONT_LIGHT, 24, tx, C4F_T_ROW_MOTION + 40, C4F_TEXT, "Acceleration (G)");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 24, tx, C4F_T_ROW_MOTION + 76, C4F_TEXT, "Gyro (rad/s)");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 24, tx, C4F_T_ROW_MOTION + 112, C4F_TEXT, "Orientation");
|
||||
c4fText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_LIGHT, C4F_MUTED, "Light bar");
|
||||
c4fTesterText(&c, C4F_FONT_LIGHT, 24, tx, C4F_T_ROW_MOTION + 40, 224, C4F_TEXT, "Acceleration (G)");
|
||||
c4fTesterText(&c, C4F_FONT_LIGHT, 24, tx, C4F_T_ROW_MOTION + 76, 224, C4F_TEXT, "Gyro (rad/s)");
|
||||
c4fTesterText(&c, C4F_FONT_LIGHT, 24, tx, C4F_T_ROW_MOTION + 112, 224, C4F_TEXT, "Orientation");
|
||||
c4fTesterText(&c, C4F_FONT_REGULAR, 24, tx, C4F_T_ROW_LIGHT, 128, C4F_MUTED, "Light bar");
|
||||
|
||||
c4fText(&c, C4F_FONT_LIGHT, 20, C4F_T_LEFT, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
|
||||
c4fTextRight(&c, C4F_FONT_LIGHT, 23, C4F_T_RIGHT, 1030, C4F_MUTED,
|
||||
@@ -703,20 +724,23 @@ static C4fColor c4fStateColor(int state)
|
||||
static void c4fDrawTabs(C4fCanvas *c, const C4fTesterView *v)
|
||||
{
|
||||
const float y = C4F_T_TABS_Y, h = 50;
|
||||
float x = C4F_T_LEFT;
|
||||
float x = c4fTextIsRTL() ? C4F_T_RIGHT : C4F_T_LEFT;
|
||||
int any = 0;
|
||||
for (int i = 0; i < C4F_TESTER_USERS; i++) {
|
||||
if (!v->user[i].used) continue;
|
||||
char generated[128];
|
||||
const char *name = c4fTesterUserName(v, i, generated, sizeof(generated));
|
||||
int on = i == v->selected;
|
||||
float w = c4fTextWidth(C4F_FONT_REGULAR, 26, v->user[i].name) + 84;
|
||||
float w = c4fTextWidthRaw(C4F_FONT_REGULAR, 26, name) + 84;
|
||||
if (c4fTextIsRTL()) x -= w;
|
||||
any = 1;
|
||||
c4fFillRoundRect(c, x, y, w, h, 6 * C4F_S, on ? C4F_RGBA(40, 103, 205, .85f) : C4F_TILE);
|
||||
c4fInsetRoundRect(c, x, y, w, h, 6 * C4F_S, on ? 3 : 1 * C4F_S, on ? C4F_ACCENT : C4F_LINE);
|
||||
c4fDot(c, x + 28, y + h / 2, c4fStateColor(v->user[i].state), v->user[i].state == C4F_TESTER_READY);
|
||||
c4fText(c, C4F_FONT_REGULAR, 26, x + 52, y + h / 2 + 9, on ? C4F_TEXT : C4F_MUTED, v->user[i].name);
|
||||
x += w + 16;
|
||||
c4fDot(c, c4fTextIsRTL() ? x + w - 28 : x + 28, y + h / 2, c4fStateColor(v->user[i].state), v->user[i].state == C4F_TESTER_READY);
|
||||
c4fTextRaw(c, C4F_FONT_REGULAR, 26, c4fTextIsRTL() ? x + 32 : x + 52, y + h / 2 + 9, on ? C4F_TEXT : C4F_MUTED, name);
|
||||
x += c4fTextIsRTL() ? -16 : w + 16;
|
||||
}
|
||||
if (!any) c4fText(c, C4F_FONT_LIGHT, 26, C4F_T_LEFT, y + 34, C4F_MUTED, "Waiting for the PS4's signed-in users...");
|
||||
if (!any) c4fTesterText(c, C4F_FONT_LIGHT, 26, C4F_T_LEFT, y + 34, C4F_T_RIGHT - C4F_T_LEFT, C4F_MUTED, "Waiting for the PS4's signed-in users...");
|
||||
}
|
||||
|
||||
/* A pressed plastic control: lit in the accent colour, like the page's .on. */
|
||||
@@ -988,7 +1012,9 @@ static void c4fDrawTouchpad(C4fCanvas *c, C4fColor accent, const char *line1, co
|
||||
const C4fBox pad = c4fControl(C4F_C_TOUCHPAD);
|
||||
if (c4fLightStrip.px) c4fBlitSprite(c, &c4fLightStrip, 0, 0);
|
||||
else c4fPaintLightStrip(c, accent);
|
||||
if (line1) c4fTextCentered(c, C4F_FONT_LIGHT, 22, pad.x + pad.w / 2, pad.y + pad.h * .9f - 30, C4F_P_ENGRAVED, line1);
|
||||
if (line1) c4fTextRaw(c, C4F_FONT_LIGHT, 22,
|
||||
pad.x + (pad.w - c4fTextWidthRaw(C4F_FONT_LIGHT, 22, line1)) / 2,
|
||||
pad.y + pad.h * .9f - 30, C4F_P_ENGRAVED, line1);
|
||||
if (line2) c4fTextCentered(c, C4F_FONT_LIGHT, 20, pad.x + pad.w / 2, pad.y + pad.h * .9f - 4,
|
||||
C4F_RGBA(236, 240, 248, .36f), line2);
|
||||
}
|
||||
@@ -1053,8 +1079,8 @@ static void c4fDrawConnection(C4fCanvas *c, const C4fTesterView *v, const C4fTes
|
||||
int column = 0;
|
||||
for (int slot = 0; slot < 4; slot++) {
|
||||
if (!(connection->pads & (1U << slot))) continue;
|
||||
char text[40];
|
||||
if (connection->rttMs[slot] < 0) snprintf(text, sizeof(text), "C%d measuring...", slot + 1);
|
||||
char text[128];
|
||||
if (connection->rttMs[slot] < 0) snprintf(text, sizeof(text), c4fTr("C%d measuring..."), slot + 1);
|
||||
else snprintf(text, sizeof(text), "C%d %d ms", slot + 1, connection->rttMs[slot]);
|
||||
c4fText(c, C4F_FONT_REGULAR, 22, values + column++ * 176, baseline,
|
||||
connection->rttMs[slot] < 0 ? C4F_MUTED : C4F_TEXT, text);
|
||||
@@ -1078,7 +1104,9 @@ static int c4fHasToolsKey;
|
||||
|
||||
static C4fBox c4fToolBox(int item)
|
||||
{
|
||||
return (C4fBox){ C4F_T_LEFT + (item % C4F_TESTER_MENU_COLUMNS) * (C4F_TOOL_W + C4F_TOOL_GAP_X),
|
||||
int column = item % C4F_TESTER_MENU_COLUMNS;
|
||||
if (c4fTextIsRTL()) column = C4F_TESTER_MENU_COLUMNS - 1 - column;
|
||||
return (C4fBox){ C4F_T_LEFT + column * (C4F_TOOL_W + C4F_TOOL_GAP_X),
|
||||
C4F_TOOL_Y + (item / C4F_TESTER_MENU_COLUMNS) * (C4F_TOOL_H + C4F_TOOL_GAP_Y),
|
||||
C4F_TOOL_W, C4F_TOOL_H };
|
||||
}
|
||||
@@ -1132,9 +1160,11 @@ static void c4fPaintToolTile(C4fCanvas *c, int item, int focused, int available)
|
||||
c4fInsetRoundRect(c, b.x, b.y, b.w, b.h, 10, focused ? 3 : 1.5f,
|
||||
focused ? C4F_ACCENT : available ? C4F_LINE : C4F_LINE_SOFT);
|
||||
C4fColor icon = available ? (focused ? C4F_TEXT : C4F_CROSS_KEY) : C4F_FAINT;
|
||||
c4fToolIcon(c, item, b.x + 28, b.y + 29, icon);
|
||||
c4fText(c, C4F_FONT_REGULAR, 30, b.x + 26, b.y + 137, available ? C4F_TEXT : C4F_FAINT, labels[item]);
|
||||
c4fText(c, C4F_FONT_LIGHT, 22, b.x + 26, b.y + 174, available ? C4F_MUTED : C4F_FAINT, c4fToolHelp(item, available));
|
||||
c4fToolIcon(c, item, c4fTextIsRTL() ? b.x + b.w - 92 : b.x + 28, b.y + 29, icon);
|
||||
c4fTesterText(c, C4F_FONT_REGULAR, 30, b.x + 26, b.y + 137, b.w - 52,
|
||||
available ? C4F_TEXT : C4F_FAINT, labels[item]);
|
||||
c4fTextWrapped(c, C4F_FONT_LIGHT, 22, b.x + 26, b.y + 174, b.w - 52, 26, 2,
|
||||
available ? C4F_MUTED : C4F_FAINT, c4fToolHelp(item, available));
|
||||
if (focused) {
|
||||
c4fFillRoundRect(c, b.x + 26, b.y + b.h - 16, b.w - 52, 3, 1.5f, C4F_ACCENT);
|
||||
c4fFillCircle(c, b.x + b.w - 28, b.y + 32, 4, C4F_TEXT);
|
||||
@@ -1188,10 +1218,11 @@ static int c4fBuildTools(C4fCanvas *work)
|
||||
|
||||
static void c4fDrawToolsDetails(C4fCanvas *c, const C4fTesterView *v, const C4fTesterConnection *connection)
|
||||
{
|
||||
c4fText(c, C4F_FONT_LIGHT, 48, C4F_T_LEFT, 154, C4F_TEXT, v->colorMenu ? "Light bar" : "Controller tools");
|
||||
c4fText(c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 202, C4F_MUTED, v->colorMenu ?
|
||||
c4fTesterText(c, C4F_FONT_LIGHT, 48, C4F_T_LEFT, 154, C4F_T_RIGHT - C4F_T_LEFT, C4F_TEXT, v->colorMenu ? "Light bar" : "Controller tools");
|
||||
c4fTesterText(c, C4F_FONT_LIGHT, 24, c4fTextIsRTL() ? C4F_T_RIGHT - 1100 : C4F_T_LEFT, 202, 1100, C4F_MUTED, v->colorMenu ?
|
||||
"Mix red, green and blue. Changes preview on the selected controller." : "Test motion, vibration and the light bar.");
|
||||
if (!v->colorMenu) c4fTextRight(c, C4F_FONT_LIGHT, 24, C4F_T_RIGHT, 202, C4F_MUTED, "D-pad or arrows to move");
|
||||
if (!v->colorMenu) c4fTesterText(c, C4F_FONT_LIGHT, 24, c4fTextIsRTL() ? C4F_T_LEFT : C4F_T_RIGHT - 470,
|
||||
202, 470, C4F_MUTED, "D-pad or arrows to move");
|
||||
const C4fHint hints[2] = { { C4F_KEY_CROSS, v->colorMenu ? (v->colorFine ? "Fast adjustment" : "Fine adjustment") : "Select" },
|
||||
{ C4F_KEY_CIRCLE, v->colorMenu ? "Done" : "Back" } };
|
||||
c4fHints(c, C4F_T_RIGHT, 1022, hints, 2);
|
||||
@@ -1199,32 +1230,43 @@ static void c4fDrawToolsDetails(C4fCanvas *c, const C4fTesterView *v, const C4fT
|
||||
"Up/Down: channel / Left/Right: adjust / OK: step size / Back: done" : "TV remote: OK select / Back return");
|
||||
const int selected = v->selected >= 0 && v->selected < C4F_TESTER_USERS;
|
||||
const int state = selected ? v->user[v->selected].state : -1;
|
||||
const char *name = selected ? v->user[v->selected].name : "No one signed in", *status;
|
||||
char detail[96];
|
||||
char generatedName[128];
|
||||
const char *name = selected ? c4fTesterUserName(v, v->selected, generatedName, sizeof(generatedName)) : "No one signed in", *status;
|
||||
char detail[256];
|
||||
if (state == C4F_TESTER_READY) status = "Controller connected";
|
||||
else if (state == C4F_TESTER_NO_CONTROLLER) status = "No controller connected";
|
||||
else if (state == C4F_TESTER_SIGNED_OUT) status = "Signed out";
|
||||
else if (state == C4F_TESTER_FAILED) {
|
||||
snprintf(detail, sizeof(detail), "Cannot read (0x%08x)", (unsigned)v->user[v->selected].error);
|
||||
snprintf(detail, sizeof(detail), c4fTr("Cannot read (0x%08x)"), (unsigned)v->user[v->selected].error);
|
||||
status = detail;
|
||||
} else status = selected ? "Opening controller..." : "Waiting for a signed-in user";
|
||||
c4fText(c, C4F_FONT_LIGHT, 20, C4F_T_LEFT, 257, C4F_FAINT, "SELECTED USER");
|
||||
c4fText(c, C4F_FONT_REGULAR, 30, C4F_T_LEFT, 300, C4F_TEXT, name);
|
||||
c4fText(c, C4F_FONT_LIGHT, 20, 620, 257, C4F_FAINT, "CONTROLLER");
|
||||
c4fDot(c, 629, 290, c4fStateColor(state), 0);
|
||||
c4fText(c, C4F_FONT_REGULAR, 26, 650, 300, state == C4F_TESTER_FAILED ? C4F_BAD : C4F_MUTED, status);
|
||||
c4fText(c, C4F_FONT_LIGHT, 20, 1240, 257, C4F_FAINT, "TEST OUTPUT");
|
||||
float userX = c4fTextIsRTL() ? 1330 : C4F_T_LEFT, statusX = c4fTextIsRTL() ? 720 : 620;
|
||||
float outputX = c4fTextIsRTL() ? C4F_T_LEFT : 1240;
|
||||
c4fTesterText(c, C4F_FONT_LIGHT, 20, userX, 257, 440, C4F_FAINT, "SELECTED USER");
|
||||
if (selected) {
|
||||
float width = c4fTextWidthRaw(C4F_FONT_REGULAR, 30, name);
|
||||
float size = width > 440 ? floorf(30 * 440 / width) : 30;
|
||||
float at = userX + (c4fTextIsRTL() ? 440 - c4fTextWidthRaw(C4F_FONT_REGULAR, size, name) : 0);
|
||||
c4fTextRaw(c, C4F_FONT_REGULAR, size, at, 300, C4F_TEXT, name);
|
||||
} else c4fTesterText(c, C4F_FONT_REGULAR, 30, userX, 300, 440, C4F_TEXT, name);
|
||||
c4fTesterText(c, C4F_FONT_LIGHT, 20, statusX, 257, 560, C4F_FAINT, "CONTROLLER");
|
||||
c4fDot(c, c4fTextIsRTL() ? statusX + 551 : statusX + 9, 290, c4fStateColor(state), 0);
|
||||
c4fTesterText(c, C4F_FONT_REGULAR, 26, statusX + (c4fTextIsRTL() ? 0 : 30), 300, 530,
|
||||
state == C4F_TESTER_FAILED ? C4F_BAD : C4F_MUTED, status);
|
||||
c4fTesterText(c, C4F_FONT_LIGHT, 20, outputX, 257, 530, C4F_FAINT, "TEST OUTPUT");
|
||||
if (v->lightSet) {
|
||||
const C4fColor light = { v->light[0], v->light[1], v->light[2], 255 };
|
||||
c4fFillRoundRect(c, 1240, 278, 28, 18, 4, light);
|
||||
c4fText(c, C4F_FONT_LIGHT, 24, 1281, 294, C4F_MUTED, "Light bar set by this test");
|
||||
} else c4fText(c, C4F_FONT_LIGHT, 24, 1240, 294, C4F_MUTED, "Light bar: PS4 default");
|
||||
if (v->rumble) c4fText(c, C4F_FONT_REGULAR, 22, 1240, 322, C4F_WARN,
|
||||
c4fFillRoundRect(c, c4fTextIsRTL() ? outputX + 502 : outputX, 278, 28, 18, 4, light);
|
||||
c4fTesterText(c, C4F_FONT_LIGHT, 24, outputX + (c4fTextIsRTL() ? 0 : 41), 294, 489, C4F_MUTED, "Light bar set by this test");
|
||||
} else c4fTesterText(c, C4F_FONT_LIGHT, 24, outputX, 294, 530, C4F_MUTED, "Light bar: PS4 default");
|
||||
if (v->rumble) c4fTesterText(c, C4F_FONT_REGULAR, 22, outputX, 322, 530, C4F_WARN,
|
||||
v->rumble == 1 ? "Large motor running" : "Small motor running");
|
||||
c4fDrawConnection(c, v, connection, C4F_T_LEFT, 329);
|
||||
const int focus = v->focus >= 0 && v->focus < C4F_TESTER_ITEMS ? v->focus : C4F_TESTER_RESUME;
|
||||
const int available = c4fTesterItemAvailable(v, focus);
|
||||
const char *note = v->note[0] ? v->note : v->colorMenu ? "The light bar returns to its PS4 color when you leave the tester." :
|
||||
char translatedNote[C4F_MESSAGE_RENDER_SIZE];
|
||||
c4fMessageRender(&v->noteText, translatedNote, sizeof(translatedNote));
|
||||
const char *note = v->noteText.key ? translatedNote : v->note[0] ? v->note : v->colorMenu ? "The light bar returns to its PS4 color when you leave the tester." :
|
||||
available ? "Choose a tool, or resume testing to see live input." :
|
||||
focus == C4F_TESTER_USER ? "Sign in another PS4 user to switch players here." :
|
||||
"Connect this user's controller to use this tool.";
|
||||
@@ -1306,10 +1348,40 @@ static void c4fDrawColorPicker(C4fCanvas *c, const C4fTesterView *v)
|
||||
char value[32];
|
||||
snprintf(value, sizeof(value), "#%02X%02X%02X", v->light[0], v->light[1], v->light[2]);
|
||||
c4fText(c, C4F_FONT_REGULAR, 38, 1324, 701, C4F_TEXT, value);
|
||||
snprintf(value, sizeof(value), "Step: %d", v->colorFine ? 1 : 16);
|
||||
snprintf(value, sizeof(value), c4fTr("Step: %d"), v->colorFine ? 1 : 16);
|
||||
c4fText(c, C4F_FONT_REGULAR, 28, 1324, 753, C4F_TEXT, value);
|
||||
}
|
||||
|
||||
/* The still layers carry words: a new language draws them again. */
|
||||
static void c4fForgetOtherLanguage(void)
|
||||
{
|
||||
const unsigned now = c4fTextTranslationGeneration();
|
||||
if (now == c4fLayersTranslation) return;
|
||||
c4fLayersTranslation = now;
|
||||
free(c4fLayer);
|
||||
c4fLayer = NULL;
|
||||
if (c4fToolsBase && c4fToolsBaseReady) {
|
||||
C4fCanvas base = { c4fToolsBase, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
|
||||
c4fPaintBackdrop(&base, C4F_S);
|
||||
c4fToolsBaseReady = 0;
|
||||
}
|
||||
free(c4fToolsFrame);
|
||||
c4fToolsFrame = NULL;
|
||||
for (int i = 0; i < C4F_TESTER_ITEMS; i++)
|
||||
for (int variant = 0; variant < 4; variant++) {
|
||||
free(c4fToolTiles[i][variant].px);
|
||||
c4fToolTiles[i][variant].px = NULL;
|
||||
}
|
||||
for (int channel = 0; channel < 3; channel++)
|
||||
for (int focused = 0; focused < 2; focused++) {
|
||||
free(c4fColorRows[channel][focused].px);
|
||||
c4fColorRows[channel][focused].px = NULL;
|
||||
}
|
||||
free(c4fColorPreview.px);
|
||||
c4fColorPreview.px = NULL;
|
||||
c4fHasToolsKey = 0;
|
||||
}
|
||||
|
||||
static void c4fDrawMenu(C4fCanvas *c, const C4fTesterView *v, const C4fTesterConnection *connection)
|
||||
{
|
||||
const size_t bytes = (size_t)c->w * c->h * sizeof(uint32_t);
|
||||
@@ -1327,12 +1399,14 @@ static void c4fDrawMenu(C4fCanvas *c, const C4fTesterView *v, const C4fTesterCon
|
||||
key.colorMenu = v->colorMenu; key.colorChannel = v->colorChannel; key.colorFine = v->colorFine;
|
||||
memcpy(key.light, v->light, sizeof(key.light));
|
||||
memcpy(key.note, v->note, sizeof(key.note));
|
||||
key.noteText = v->noteText;
|
||||
C4fTesterConnection connectionKey = { 0 };
|
||||
connectionKey.available = connection->available; connectionKey.pads = connection->pads;
|
||||
memcpy(connectionKey.rttMs, connection->rttMs, sizeof(connectionKey.rttMs));
|
||||
unsigned available = 0;
|
||||
for (int i = 0; i < C4F_TESTER_USERS; i++) {
|
||||
key.user[i].used = v->user[i].used; key.user[i].state = v->user[i].state; key.user[i].error = v->user[i].error;
|
||||
key.user[i].nameGenerated = v->user[i].nameGenerated;
|
||||
memcpy(key.user[i].name, v->user[i].name, sizeof(key.user[i].name));
|
||||
}
|
||||
for (int i = 0; i < C4F_TESTER_ITEMS; i++) if (c4fTesterItemAvailable(v, i)) available |= 1U << i;
|
||||
@@ -1366,6 +1440,7 @@ void c4fDrawTester(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
const C4fTesterView *v = &s->tester;
|
||||
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
|
||||
const size_t bytes = (size_t)c.w * (size_t)c.h * sizeof(uint32_t);
|
||||
c4fForgetOtherLanguage();
|
||||
if (v->menu) { c4fDrawMenu(&c, v, &s->connection); return; }
|
||||
const int state = v->selected >= 0 ? v->user[v->selected].state : -1, ready = state == C4F_TESTER_READY;
|
||||
const C4fColor accent = v->lightSet ? (C4fColor){ v->light[0], v->light[1], v->light[2], 255 } : C4F_ACCENT;
|
||||
@@ -1376,8 +1451,9 @@ void c4fDrawTester(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
|
||||
c4fDrawTabs(&c, v);
|
||||
/* The page lights the controller in its colour; here, the colour this test set. */
|
||||
char line[96];
|
||||
const char *name = v->selected >= 0 ? v->user[v->selected].name : "", *detail;
|
||||
char line[256];
|
||||
char generatedName[128];
|
||||
const char *name = v->selected >= 0 ? c4fTesterUserName(v, v->selected, generatedName, sizeof(generatedName)) : "", *detail;
|
||||
if (ready) {
|
||||
c4fDrawInput(&c, v, accent);
|
||||
detail = "Reading this user's controller";
|
||||
@@ -1386,10 +1462,10 @@ void c4fDrawTester(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
c4fFillRoundRect(&c, C4F_P_X, C4F_P_Y, C4F_P_W, C4F_P_H, 12, C4F_RGBA(2, 6, 14, .55f));
|
||||
if (state == C4F_TESTER_NO_CONTROLLER) detail = "No controller connected";
|
||||
else if (state == C4F_TESTER_FAILED) {
|
||||
snprintf(line, sizeof(line), "The PS4 would not read it (0x%08x)", (unsigned)v->user[v->selected].error);
|
||||
snprintf(line, sizeof(line), c4fTr("The PS4 would not read it (0x%08x)"), (unsigned)v->user[v->selected].error);
|
||||
detail = line;
|
||||
} else if (state == C4F_TESTER_SIGNED_OUT) detail = "Signed out";
|
||||
else if (state < 0) { name = "No one is signed in"; detail = NULL; }
|
||||
else if (state < 0) { name = c4fTr("No one is signed in"); detail = NULL; }
|
||||
else detail = "Opening this user's controller...";
|
||||
}
|
||||
c4fDrawTouchpad(&c, accent, name, detail);
|
||||
@@ -1398,7 +1474,10 @@ void c4fDrawTester(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
if (v->rumble)
|
||||
c4fText(&c, C4F_FONT_REGULAR, 26, C4F_P_X + 4, C4F_T_TOP + C4F_T_HEIGHT - 30, C4F_WARN,
|
||||
v->rumble == 1 ? "Rumbling: large motor" : "Rumbling: small motor");
|
||||
if (v->note[0]) c4fText(&c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 972, C4F_TEXT, v->note);
|
||||
else c4fText(&c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 972, C4F_MUTED,
|
||||
char translatedNote[C4F_MESSAGE_RENDER_SIZE];
|
||||
c4fMessageRender(&v->noteText, translatedNote, sizeof(translatedNote));
|
||||
if (v->note[0]) c4fTesterText(&c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 972, C4F_T_RIGHT - C4F_T_LEFT,
|
||||
C4F_TEXT, v->noteText.key ? translatedNote : v->note);
|
||||
else c4fTesterText(&c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 972, C4F_T_RIGHT - C4F_T_LEFT, C4F_MUTED,
|
||||
"Hold L3 + R3 for two seconds for the menu: change user, rumble, light bar, leave.");
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
/* Unicode layout: the Unicode bidi algorithm supplies visual runs; HarfBuzz
|
||||
* shapes each run; the existing stb rasterizer draws the resulting glyph IDs.
|
||||
* All caches are bounded and belong exclusively to the drawing thread. */
|
||||
#include "text.h"
|
||||
#include "stb_truetype.h"
|
||||
#include <hb.h>
|
||||
#include <hb-ot.h>
|
||||
#include <SheenBidi/SheenBidi.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
extern const unsigned char c4fFontArabicLight[], c4fFontArabicRegular[];
|
||||
extern const size_t c4fFontArabicLightSize, c4fFontArabicRegularSize;
|
||||
enum { C4F_LAYOUTS = 32, C4F_LAYOUT_BYTES = 2048, C4F_LAYOUT_GLYPHS = 1024,
|
||||
C4F_RASTERS = 512, C4F_RASTER_BYTES = 4 * 1024 * 1024 };
|
||||
typedef struct { unsigned id; float x, y; } C4fPositionedGlyph;
|
||||
typedef struct {
|
||||
char text[C4F_LAYOUT_BYTES];
|
||||
int length, font, pixels, rtl, count, ready;
|
||||
unsigned used;
|
||||
float width;
|
||||
C4fPositionedGlyph glyphs[C4F_LAYOUT_GLYPHS];
|
||||
} C4fTextLayout;
|
||||
typedef struct {
|
||||
int font, pixels, w, h, x, y, ready;
|
||||
unsigned glyph, used;
|
||||
unsigned char *bitmap;
|
||||
} C4fGlyphRaster;
|
||||
static hb_font_t *c4fArabicHB[C4F_FONT_COUNT];
|
||||
static stbtt_fontinfo c4fArabicSTB[C4F_FONT_COUNT];
|
||||
static C4fTextLayout c4fLayouts[C4F_LAYOUTS];
|
||||
static C4fGlyphRaster c4fRasters[C4F_RASTERS];
|
||||
static size_t c4fRasterBytes;
|
||||
static unsigned c4fTextTick;
|
||||
static int c4fRTL;
|
||||
static uint8_t c4fGlyphGamma[256];
|
||||
static const unsigned char *c4fArabicData[C4F_FONT_COUNT];
|
||||
static size_t c4fArabicSizes[C4F_FONT_COUNT];
|
||||
static int c4fSystemFontsLoaded;
|
||||
|
||||
void c4fSetTextDirection(int rtl) { c4fRTL = !!rtl; }
|
||||
int c4fTextIsRTL(void) { return c4fRTL; }
|
||||
|
||||
static unsigned c4fBE16(const unsigned char *p) { return (unsigned)p[0] << 8 | p[1]; }
|
||||
static uint32_t c4fBE32(const unsigned char *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; }
|
||||
|
||||
static int c4fValidFont(const unsigned char *data, size_t size)
|
||||
{
|
||||
if (!data || size < 12 || size > 1024 * 1024 ||
|
||||
(memcmp(data, "OTTO", 4) && c4fBE32(data) != 0x00010000)) return 0;
|
||||
unsigned tables = c4fBE16(data + 4);
|
||||
if (!tables || (size_t)tables > (size - 12) / 16) return 0;
|
||||
for (unsigned i = 0; i < tables; i++) {
|
||||
const unsigned char *entry = data + 12 + 16 * i;
|
||||
uint32_t offset = c4fBE32(entry + 8), length = c4fBE32(entry + 12);
|
||||
if (offset > size || length > size - offset) return 0;
|
||||
}
|
||||
stbtt_fontinfo info;
|
||||
return stbtt_InitFont(&info, data, 0) && stbtt_FindGlyphIndex(&info, 0x0628) != 0;
|
||||
}
|
||||
|
||||
int c4fTextSetArabicFonts(const unsigned char *light, size_t lightSize,
|
||||
const unsigned char *regular, size_t regularSize)
|
||||
{
|
||||
if (!c4fValidFont(light, lightSize) || !c4fValidFont(regular, regularSize)) return -1;
|
||||
for (int i = 0; i < C4F_FONT_COUNT; i++) {
|
||||
if (c4fArabicHB[i]) hb_font_destroy(c4fArabicHB[i]);
|
||||
c4fArabicHB[i] = NULL;
|
||||
}
|
||||
for (int i = 0; i < C4F_RASTERS; i++) free(c4fRasters[i].bitmap);
|
||||
memset(c4fRasters, 0, sizeof(c4fRasters)); c4fRasterBytes = 0;
|
||||
memset(c4fLayouts, 0, sizeof(c4fLayouts));
|
||||
c4fArabicData[0] = light; c4fArabicData[1] = regular;
|
||||
c4fArabicSizes[0] = lightSize; c4fArabicSizes[1] = regularSize;
|
||||
int result = c4fShapedTextInit();
|
||||
c4fTextFontChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
static unsigned char *c4fReadFont(const char *path, size_t *size)
|
||||
{
|
||||
FILE *file = fopen(path, "rb");
|
||||
if (!file) return NULL;
|
||||
unsigned char *bytes = NULL;
|
||||
if (!fseek(file, 0, SEEK_END)) {
|
||||
long length = ftell(file);
|
||||
if (length >= 12 && length <= 1024 * 1024 && !fseek(file, 0, SEEK_SET)) {
|
||||
bytes = malloc((size_t)length);
|
||||
if (bytes && fread(bytes, 1, (size_t)length, file) == (size_t)length && c4fValidFont(bytes, (size_t)length)) *size = (size_t)length;
|
||||
else { free(bytes); bytes = NULL; }
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
int c4fTextLoadSystemFonts(void)
|
||||
{
|
||||
if (c4fSystemFontsLoaded) return 0;
|
||||
static const char *const lightPaths[] = { "/preinst/common/font/SSTArabic-Light.otf", "/system/common/font2/SSTArabic-Light.otf" };
|
||||
static const char *const regularPaths[] = { "/preinst/common/font/SSTArabic-Roman.otf", "/system/common/font2/SSTArabic-Roman.otf" };
|
||||
unsigned char *bytes[C4F_FONT_COUNT] = { NULL, NULL };
|
||||
size_t sizes[C4F_FONT_COUNT] = { 0, 0 };
|
||||
for (unsigned i = 0; i < 2 && !bytes[0]; i++) bytes[0] = c4fReadFont(lightPaths[i], &sizes[0]);
|
||||
for (unsigned i = 0; i < 2 && !bytes[1]; i++) bytes[1] = c4fReadFont(regularPaths[i], &sizes[1]);
|
||||
if (bytes[0] && bytes[1] && !c4fTextSetArabicFonts(bytes[0], sizes[0], bytes[1], sizes[1])) {
|
||||
/* Retain these bounded, read-only files for the process lifetime. */
|
||||
c4fSystemFontsLoaded = 1;
|
||||
return 0;
|
||||
}
|
||||
free(bytes[0]); free(bytes[1]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int c4fShapedTextInit(void)
|
||||
{
|
||||
const unsigned char *data[] = { c4fArabicData[0] ? c4fArabicData[0] : c4fFontArabicLight,
|
||||
c4fArabicData[1] ? c4fArabicData[1] : c4fFontArabicRegular };
|
||||
const size_t sizes[] = { c4fArabicData[0] ? c4fArabicSizes[0] : c4fFontArabicLightSize,
|
||||
c4fArabicData[1] ? c4fArabicSizes[1] : c4fFontArabicRegularSize };
|
||||
for (int i = 0; i < C4F_FONT_COUNT; i++) {
|
||||
if (c4fArabicHB[i]) continue;
|
||||
if (!stbtt_InitFont(&c4fArabicSTB[i], data[i], 0)) return -1;
|
||||
hb_blob_t *blob = hb_blob_create((const char *)data[i], (unsigned)sizes[i], HB_MEMORY_MODE_READONLY, NULL, NULL);
|
||||
hb_face_t *face = hb_face_create(blob, 0);
|
||||
c4fArabicHB[i] = hb_font_create(face);
|
||||
hb_ot_font_set_funcs(c4fArabicHB[i]);
|
||||
unsigned em = hb_face_get_upem(face);
|
||||
hb_font_set_scale(c4fArabicHB[i], (int)em, (int)em);
|
||||
hb_face_destroy(face);
|
||||
hb_blob_destroy(blob);
|
||||
}
|
||||
for (int i = 0; i < 256; i++) c4fGlyphGamma[i] = (uint8_t)(powf(i / 255.0f, .8f) * 255.0f + .5f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int c4fTextNeedsShaping(const char *text, int length)
|
||||
{
|
||||
/* ASCII and Latin-1 keep the original fast Roboto path. All other Unicode,
|
||||
* including direction isolates in translated templates, uses this path. */
|
||||
for (int i = 0; i < length && text[i]; i++)
|
||||
if ((unsigned char)text[i] >= 0xc4) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static C4fTextLayout *c4fShape(int font, float size, const char *text, int length)
|
||||
{
|
||||
if (!text || length < 0 || font < 0 || font >= C4F_FONT_COUNT || !c4fArabicHB[font]) return NULL;
|
||||
int pixels = (int)(size + .5f);
|
||||
if (pixels <= 0 || pixels > 256) return NULL;
|
||||
if (length >= C4F_LAYOUT_BYTES) {
|
||||
length = C4F_LAYOUT_BYTES - 1;
|
||||
while (length && ((unsigned char)text[length] & 0xc0) == 0x80) length--;
|
||||
}
|
||||
C4fTextLayout *out = &c4fLayouts[0];
|
||||
for (int i = 0; i < C4F_LAYOUTS; i++) {
|
||||
C4fTextLayout *p = &c4fLayouts[i];
|
||||
if (p->ready && p->font == font && p->pixels == pixels && p->rtl == c4fRTL &&
|
||||
p->length == length && !memcmp(p->text, text, (size_t)length)) {
|
||||
p->used = ++c4fTextTick;
|
||||
return p;
|
||||
}
|
||||
if (!p->ready || (out->ready && p->used < out->used)) out = p;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
memcpy(out->text, text, (size_t)length);
|
||||
out->length = length; out->font = font; out->pixels = pixels; out->rtl = c4fRTL;
|
||||
out->used = ++c4fTextTick;
|
||||
SBCodepointSequence sequence = { SBStringEncodingUTF8, out->text, (SBUInteger)length };
|
||||
SBAlgorithmRef algorithm = SBAlgorithmCreate(&sequence);
|
||||
if (!algorithm) return NULL;
|
||||
SBParagraphRef paragraph = SBAlgorithmCreateParagraph(algorithm, 0, (SBUInteger)length,
|
||||
c4fRTL ? SBLevelDefaultRTL : SBLevelDefaultLTR);
|
||||
SBLineRef line = paragraph ? SBParagraphCreateLine(paragraph, 0, SBParagraphGetLength(paragraph)) : NULL;
|
||||
if (line) {
|
||||
const SBRun *runs = SBLineGetRunsPtr(line);
|
||||
const float scale = stbtt_ScaleForMappingEmToPixels(&c4fArabicSTB[font], (float)pixels);
|
||||
for (SBUInteger i = 0; i < SBLineGetRunCount(line); i++) {
|
||||
hb_buffer_t *buffer = hb_buffer_create();
|
||||
hb_buffer_add_utf8(buffer, out->text, length, (unsigned)runs[i].offset, (int)runs[i].length);
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_buffer_set_direction(buffer, (runs[i].level & 1) ? HB_DIRECTION_RTL : HB_DIRECTION_LTR);
|
||||
hb_shape(c4fArabicHB[font], buffer, NULL, 0);
|
||||
unsigned count;
|
||||
const hb_glyph_info_t *glyphs = hb_buffer_get_glyph_infos(buffer, &count);
|
||||
const hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(buffer, NULL);
|
||||
for (unsigned j = 0; j < count && out->count < C4F_LAYOUT_GLYPHS; j++) {
|
||||
C4fPositionedGlyph *g = &out->glyphs[out->count++];
|
||||
g->id = glyphs[j].codepoint;
|
||||
g->x = out->width + positions[j].x_offset * scale;
|
||||
g->y = -positions[j].y_offset * scale;
|
||||
out->width += positions[j].x_advance * scale;
|
||||
}
|
||||
hb_buffer_destroy(buffer);
|
||||
}
|
||||
SBLineRelease(line);
|
||||
}
|
||||
if (paragraph) SBParagraphRelease(paragraph);
|
||||
SBAlgorithmRelease(algorithm);
|
||||
out->ready = line != NULL;
|
||||
return out->ready ? out : NULL;
|
||||
}
|
||||
|
||||
static void c4fFreeRaster(C4fGlyphRaster *r)
|
||||
{
|
||||
if (r->bitmap) c4fRasterBytes -= (size_t)r->w * (size_t)r->h;
|
||||
free(r->bitmap);
|
||||
memset(r, 0, sizeof(*r));
|
||||
}
|
||||
|
||||
static C4fGlyphRaster *c4fRaster(int font, int pixels, unsigned glyph)
|
||||
{
|
||||
C4fGlyphRaster *out = &c4fRasters[0];
|
||||
for (int i = 0; i < C4F_RASTERS; i++) {
|
||||
C4fGlyphRaster *r = &c4fRasters[i];
|
||||
if (r->ready && r->font == font && r->pixels == pixels && r->glyph == glyph) {
|
||||
r->used = ++c4fTextTick;
|
||||
return r;
|
||||
}
|
||||
if (!r->ready || (out->ready && r->used < out->used)) out = r;
|
||||
}
|
||||
c4fFreeRaster(out);
|
||||
const stbtt_fontinfo *face = &c4fArabicSTB[font];
|
||||
float scale = stbtt_ScaleForMappingEmToPixels(face, (float)pixels);
|
||||
int x1, y1;
|
||||
stbtt_GetGlyphBitmapBox(face, (int)glyph, scale, scale, &out->x, &out->y, &x1, &y1);
|
||||
out->w = x1 - out->x; out->h = y1 - out->y;
|
||||
size_t bytes = (size_t)out->w * (size_t)out->h;
|
||||
if (bytes > C4F_RASTER_BYTES) return NULL;
|
||||
while (c4fRasterBytes + bytes > C4F_RASTER_BYTES) {
|
||||
C4fGlyphRaster *oldest = NULL;
|
||||
for (int i = 0; i < C4F_RASTERS; i++)
|
||||
if (&c4fRasters[i] != out && c4fRasters[i].bitmap && (!oldest || c4fRasters[i].used < oldest->used)) oldest = &c4fRasters[i];
|
||||
if (!oldest) return NULL;
|
||||
c4fFreeRaster(oldest);
|
||||
}
|
||||
if (bytes) {
|
||||
out->bitmap = malloc(bytes);
|
||||
if (!out->bitmap) return NULL;
|
||||
stbtt_MakeGlyphBitmap(face, out->bitmap, out->w, out->h, out->w, scale, scale, (int)glyph);
|
||||
c4fRasterBytes += bytes;
|
||||
}
|
||||
out->font = font; out->pixels = pixels; out->glyph = glyph;
|
||||
out->used = ++c4fTextTick; out->ready = 1;
|
||||
return out;
|
||||
}
|
||||
|
||||
float c4fShapedText(C4fCanvas *c, int font, float size, float x, float y,
|
||||
C4fColor color, const char *text, int length)
|
||||
{
|
||||
C4fTextLayout *layout = c4fShape(font, size, text, length);
|
||||
if (!layout) return 0;
|
||||
if (c) for (int i = 0; i < layout->count; i++) {
|
||||
const C4fPositionedGlyph *g = &layout->glyphs[i];
|
||||
C4fGlyphRaster *r = c4fRaster(font, layout->pixels, g->id);
|
||||
if (!r || !r->bitmap) continue;
|
||||
int left = (int)floorf(x + g->x + .5f) + r->x, top = (int)floorf(y + g->y + .5f) + r->y;
|
||||
for (int row = 0; row < r->h; row++) for (int col = 0; col < r->w; col++) {
|
||||
unsigned value = r->bitmap[row * r->w + col];
|
||||
if (value) c4fBlend(c, left + col, top + row, color, c4fGlyphGamma[value] / 255.0f);
|
||||
}
|
||||
}
|
||||
return layout->width;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef C4F_LAUNCHER_TEXT_H
|
||||
#define C4F_LAUNCHER_TEXT_H
|
||||
#include "draw.h"
|
||||
/* Internal Unicode layout path. Called only by the drawing thread. */
|
||||
int c4fShapedTextInit(void);
|
||||
int c4fTextNeedsShaping(const char *text, int length);
|
||||
float c4fShapedText(C4fCanvas *c, int font, float size, float x, float y,
|
||||
C4fColor color, const char *text, int length);
|
||||
void c4fTextFontChanged(void);
|
||||
#endif
|
||||
@@ -28,6 +28,45 @@
|
||||
#define C4F_NET_LOG(c, fmt, ...) do { } while (0)
|
||||
#endif
|
||||
|
||||
/* System fonts stay on the console. Only these two read-only assets are served;
|
||||
* no request path is ever used as a filesystem path. Retain them across transport
|
||||
* reopens so font reads never enter the controller input loop. */
|
||||
#ifndef C4F_SYSTEM_FONT_PRIMARY
|
||||
#define C4F_SYSTEM_FONT_PRIMARY "/preinst/common/font/"
|
||||
#endif
|
||||
#ifndef C4F_SYSTEM_FONT_FALLBACK
|
||||
#define C4F_SYSTEM_FONT_FALLBACK "/system/common/font2/"
|
||||
#endif
|
||||
typedef struct { unsigned char *data; size_t size; } C4fSystemFont;
|
||||
static C4fSystemFont c4fSystemFonts[2];
|
||||
|
||||
static void c4fReadSystemFonts(void)
|
||||
{
|
||||
static const char *const names[2] = { "SSTArabic-Light.otf", "SSTArabic-Roman.otf" };
|
||||
static const char *const directories[2] = { C4F_SYSTEM_FONT_PRIMARY, C4F_SYSTEM_FONT_FALLBACK };
|
||||
for (int weight = 0; weight < 2; weight++) {
|
||||
if (c4fSystemFonts[weight].data) continue;
|
||||
for (int directory = 0; directory < 2; directory++) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "%s%s", directories[directory], names[weight]);
|
||||
FILE *file = fopen(path, "rb");
|
||||
if (!file) continue;
|
||||
long length = -1;
|
||||
if (!fseek(file, 0, SEEK_END)) length = ftell(file);
|
||||
if (length < 128 || length > 1024 * 1024 || fseek(file, 0, SEEK_SET)) {
|
||||
fclose(file); continue;
|
||||
}
|
||||
unsigned char *data = malloc((size_t)length);
|
||||
int valid = data && fread(data, 1, (size_t)length, file) == (size_t)length;
|
||||
fclose(file);
|
||||
if (valid) valid = !memcmp(data, "OTTO", 4) || !memcmp(data, "\0\1\0\0", 4);
|
||||
if (!valid) { free(data); continue; }
|
||||
c4fSystemFonts[weight] = (C4fSystemFont){ data, (size_t)length };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Which interface would carry traffic out, without sending anything: a connected
|
||||
* UDP socket picks the route, and getsockname then names the local end. */
|
||||
void c4fNetLocalAddress(char *out, size_t size)
|
||||
@@ -239,6 +278,15 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
int stopRequest = !strncmp(request, "POST /api/stop HTTP/1.1\r\n", 25);
|
||||
int offerRequest = !strncmp(request, "POST /rtc HTTP/1.1\r\n", 20);
|
||||
int preflightRequest = !strncmp(request, "OPTIONS /rtc HTTP/1.1\r\n", 23);
|
||||
int fontRequest = -1, fontPreflight = 0;
|
||||
static const char *const fontRoutes[2] = { "/fonts/arabic-light.otf", "/fonts/arabic-regular.otf" };
|
||||
for (int weight = 0; weight < 2; weight++) {
|
||||
char route[80];
|
||||
snprintf(route, sizeof(route), "GET %s HTTP/1.1\r\n", fontRoutes[weight]);
|
||||
if (!strncmp(request, route, strlen(route))) fontRequest = weight;
|
||||
snprintf(route, sizeof(route), "OPTIONS %s HTTP/1.1\r\n", fontRoutes[weight]);
|
||||
if (!strncmp(request, route, strlen(route))) { fontRequest = weight; fontPreflight = 1; }
|
||||
}
|
||||
int websocketRequest = 0;
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
websocketRequest = !strncmp(request, "GET /ws HTTP/1.1\r\n", 18);
|
||||
@@ -252,7 +300,7 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
} else if (!strncmp(request, "GET /icon-192.png HTTP/1.1\r\n", 28)) {
|
||||
asset = c4fIcon; assetSize = c4fIconSize; assetType = "image/png";
|
||||
}
|
||||
if (!statusRequest && !stopRequest && !offerRequest && !preflightRequest && !asset &&
|
||||
if (!statusRequest && !stopRequest && !offerRequest && !preflightRequest && fontRequest < 0 && !asset &&
|
||||
strncmp(request, "GET / HTTP/1.1\r\n", 16) &&
|
||||
strncmp(request, "GET /index.html HTTP/1.1\r\n", 26) &&
|
||||
!websocketRequest &&
|
||||
@@ -273,7 +321,30 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
c4fHttpError(c, 403, "Open the PS4 IP address"); return;
|
||||
}
|
||||
}
|
||||
if (offerRequest || preflightRequest) {
|
||||
if (fontRequest >= 0) {
|
||||
char expected[160];
|
||||
snprintf(expected, sizeof(expected), "http://%s", host);
|
||||
if (upgrade[0] || (origin[0] && strcmp(origin, "null") && strcmp(origin, expected))) {
|
||||
c4fHttpError(c, 403, "Origin not allowed"); return;
|
||||
}
|
||||
c->savedOrigin = !strcmp(origin, "null");
|
||||
if (fontPreflight) {
|
||||
if (!c->savedOrigin) { c4fHttpError(c, 403, "Origin not allowed"); return; }
|
||||
static const char reply[] = "HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: null\r\n"
|
||||
"Access-Control-Allow-Methods: GET\r\nAccess-Control-Allow-Private-Network: true\r\n"
|
||||
"Access-Control-Max-Age: 600\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
|
||||
c4fQueue(c, reply, sizeof(reply) - 1); c->closing = 2;
|
||||
return;
|
||||
}
|
||||
C4fSystemFont *font = &c4fSystemFonts[fontRequest];
|
||||
if (!font->data) { c4fHttpError(c, 404, "System font unavailable"); return; }
|
||||
char reply[384];
|
||||
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: font/otf\r\nContent-Length: %zu\r\n"
|
||||
"Cache-Control: private, max-age=86400\r\nX-Content-Type-Options: nosniff\r\n%sConnection: close\r\n\r\n",
|
||||
font->size, c->savedOrigin ? "Access-Control-Allow-Origin: null\r\n" : "");
|
||||
c4fQueue(c, reply, (size_t)n);
|
||||
c->body = font->data; c->bodySize = font->size; c->rxUsed = 0;
|
||||
} else if (offerRequest || preflightRequest) {
|
||||
c4fHttpOffer(net, c, request, used, host, origin, preflightRequest);
|
||||
} else if (statusRequest || stopRequest) {
|
||||
char launcher[8], length[16], transfer[32];
|
||||
@@ -331,7 +402,7 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
c->body = asset; c->bodySize = assetSize; c->rxUsed = 0;
|
||||
} else {
|
||||
char reply[768];
|
||||
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Encoding: gzip\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nContent-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self' data:; media-src data:; connect-src 'self'; manifest-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'\r\n\r\n", c4fPageSize);
|
||||
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Encoding: gzip\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nContent-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; media-src data:; connect-src 'self'; manifest-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'\r\n\r\n", c4fPageSize);
|
||||
C4F_NET_LOG(c, "sending the page, %zu bytes", c4fPageSize);
|
||||
c4fQueue(c, reply, (size_t)n);
|
||||
c->body = c4fPage; c->bodySize = c4fPageSize; c->rxUsed = 0;
|
||||
@@ -384,6 +455,7 @@ static void c4fWs(C4fNet *net, C4fNetClient *c)
|
||||
|
||||
int c4fNetOpen(C4fNet *net, int port, C4fNetHandler handler, void *context)
|
||||
{
|
||||
c4fReadSystemFonts();
|
||||
struct sockaddr_in addr;
|
||||
int one = 1;
|
||||
/* The certificate outlives a reopen: making one takes a moment. */
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('node:fs'), assert = require('node:assert/strict'), { spawn } = require('node:child_process');
|
||||
const { relayOffer, recordRtcMessages, copyService } = require('./rtc_browser_helpers.cjs');
|
||||
const checkControllerLayout = require('./layout_browser_test.cjs');
|
||||
const hook = '\nwindow.c4fTest = { ControllerMapping, mapper, profileStore, gamepadSources, pollGamepads, setGamepadPad, mergedState, renderGamepads, renderPads, openSettings, closeSettings, openPad, profileFor, loadProfiles, persistProfiles, clearDevice, applyStatus, status, conn, sendInput, sendMotion, getKeys: () => keymap };\n';
|
||||
const html = fs.readFileSync('/src/client/index.html', 'utf8').replace('\n })();', hook + ' })();');
|
||||
copyService('/tmp/c4f-browser-service');
|
||||
@@ -114,6 +115,7 @@ async function checkConnectionLifecycle(browser) {
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
await checkControllerLayout(browser);
|
||||
await checkConnectionLifecycle(browser);
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
||||
// A fulfilled test document has no network address-space metadata. Allow
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const assert = require('node:assert/strict');
|
||||
const tr = (key, values) => values ? key.replace(/\{(\w+)\}/g, (_, name) => values[name]) : key;
|
||||
const page = fs.readFileSync('client/index.html', 'utf8');
|
||||
const extract = (start, end) => {
|
||||
const from = page.indexOf(start);
|
||||
@@ -25,7 +26,7 @@ function padStatus(fields) {
|
||||
conn: { echoes: new Map([[7, 100]]), pending: new Map(), latency: true },
|
||||
now: () => 116.6, pingMs: 0, send: message => sentRtt.push(message), showVersion() {},
|
||||
};
|
||||
vm.createContext(pingCtx);
|
||||
vm.createContext(Object.assign(pingCtx, { tr }));
|
||||
vm.runInContext(extract(' function onMessage(data)', ' function applyStatus('), pingCtx);
|
||||
vm.runInContext('onMessage(JSON.stringify({ method: "e", params: [7, 100] }))', pingCtx);
|
||||
assert.equal(pingCtx.pingMs, 17);
|
||||
@@ -38,7 +39,7 @@ function padStatus(fields) {
|
||||
const routed = [], routeCtx = { conn: { state: 'open',
|
||||
input: { readyState: 'open', bufferedAmount: 0, send: data => routed.push(['input', JSON.parse(data)]) },
|
||||
control: { readyState: 'open', bufferedAmount: 0, send: data => routed.push(['control', JSON.parse(data)]) } } };
|
||||
vm.createContext(routeCtx);
|
||||
vm.createContext(Object.assign(routeCtx, { tr }));
|
||||
vm.runInContext(extract(' function send(obj)', ' function call('), routeCtx);
|
||||
vm.runInContext('send({ method: "latency", params: [7, 17] })', routeCtx);
|
||||
assert.equal(routed[0][0], 'input');
|
||||
@@ -62,7 +63,7 @@ function padStatus(fields) {
|
||||
},
|
||||
applyStatus() {}, toast() {}, closePad() { ctx.primaryPad = -1; }, renderGamepads() {},
|
||||
};
|
||||
vm.createContext(ctx);
|
||||
vm.createContext(Object.assign(ctx, { tr }));
|
||||
vm.runInContext(sendClaimSource, ctx);
|
||||
vm.runInContext('sendClaim(true)', ctx);
|
||||
for (let i = 0; i < 5; i++) await new Promise(setImmediate);
|
||||
@@ -85,7 +86,7 @@ function padStatus(fields) {
|
||||
serialClaims.push(params); return new Promise((resolve, reject) => deferred.push({ resolve, reject }));
|
||||
}, applyStatus() {}, toast() {}, closePad() {}, renderGamepads() {},
|
||||
};
|
||||
vm.createContext(serial); vm.runInContext(sendClaimSource, serial);
|
||||
vm.createContext(Object.assign(serial, { tr })); vm.runInContext(sendClaimSource, serial);
|
||||
vm.runInContext('sendClaim(true)', serial);
|
||||
serial.gamepadSources.get(0).pad = 2; vm.runInContext('sendClaim()', serial);
|
||||
for (const s of serial.gamepadSources.values()) s.pad = -1;
|
||||
@@ -123,7 +124,7 @@ function padStatus(fields) {
|
||||
clearDevice(src) { Object.assign(src, pads.blankSource(src.pad)); },
|
||||
mapper: { index: null, cancelCapture() {}, status() {} },
|
||||
};
|
||||
vm.createContext(pads);
|
||||
vm.createContext(Object.assign(pads, { tr }));
|
||||
vm.runInContext(pollSource, pads);
|
||||
vm.runInContext('pollGamepads(100)', pads);
|
||||
const src = pads.gamepadSources.get(0);
|
||||
@@ -151,7 +152,7 @@ function padStatus(fields) {
|
||||
toast(message) { ctx.toasts.push(message); },
|
||||
};
|
||||
ctx.window.matchMedia = ctx.matchMedia;
|
||||
vm.createContext(ctx);
|
||||
vm.createContext(Object.assign(ctx, { tr }));
|
||||
vm.runInContext(fsSource, ctx);
|
||||
if (!button.hidden) await button.handlers.click();
|
||||
return ctx;
|
||||
@@ -179,7 +180,7 @@ function padStatus(fields) {
|
||||
|
||||
// Which gamepads the page says it can rumble.
|
||||
const rumbleCtx = {};
|
||||
vm.createContext(rumbleCtx);
|
||||
vm.createContext(Object.assign(rumbleCtx, { tr }));
|
||||
vm.runInContext(extract(' function canRumble(gp)', ' function renderGamepads()'), rumbleCtx);
|
||||
const can = gp => vm.runInContext('canRumble(gp)', Object.assign(rumbleCtx, { gp }));
|
||||
const play = () => Promise.resolve();
|
||||
@@ -192,7 +193,7 @@ function padStatus(fields) {
|
||||
|
||||
// The Invite panel draws the console's grid as runs of dark modules.
|
||||
const qrCtx = {};
|
||||
vm.createContext(qrCtx);
|
||||
vm.createContext(Object.assign(qrCtx, { tr }));
|
||||
vm.runInContext(extract(' function qrPath(size, rows, quiet)', ' // The address this page reached'), qrCtx);
|
||||
// 5 wide: row 0 = 11011, row 1 = 00100 -> hex d8, 20.
|
||||
assert.equal(vm.runInContext("qrPath(5, ['d8', '20'], 4)", qrCtx),
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// Real DOM regressions for translation boundaries and the narrow-screen picker.
|
||||
// No service or console: instrument the shipped page in memory and block network.
|
||||
const fs = require('node:fs');
|
||||
const assert = require('node:assert/strict');
|
||||
const { chromium } = require('playwright');
|
||||
const hook = '\nwindow.c4fTest = { ControllerMapping, mapper, profileStore, gamepadSources, renderGamepads, renderPads, renderPadLabel, status, keymap, setPrimary: value => { primaryPad = value; } };\n';
|
||||
const original = fs.readFileSync('client/index.html', 'utf8');
|
||||
const html = original.replace(' connect(address);', ' // No signaling in this offline fixture.')
|
||||
.replace('\n })();', hook + ' })();');
|
||||
assert.notEqual(html, original);
|
||||
const clean = value => value.replace(/[\u2066-\u2069]/g, '');
|
||||
const settle = page => page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
for (const locale of ['en-US', 'pt-BR', 'ar-SA']) for (const width of [320, 360, 390, 1440]) {
|
||||
const language = locale.startsWith('ar') ? 'ar' : locale.startsWith('pt') ? 'pt-BR' : 'en';
|
||||
const controller = language === 'ar' ? 'يد التحكم' : language === 'pt-BR' ? 'Controle' : 'Controller';
|
||||
const gamepad = language === 'ar' ? 'يد متصلة' : 'Gamepad';
|
||||
const context = await browser.newContext({ viewport: { width, height: 900 }, locale });
|
||||
await context.addInitScript(() => {
|
||||
window.mockPad = { index: 0, id: 'Left', mapping: 'standard', connected: true,
|
||||
buttons: Array.from({ length: 18 }, () => ({ value: 0, pressed: false })), axes: [0, 0, 0, 0] };
|
||||
Object.defineProperty(navigator, 'getGamepads', { value: () => [window.mockPad] });
|
||||
});
|
||||
const page = await context.newPage(), errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.route('**/*', route => {
|
||||
const url = route.request().url();
|
||||
if (url === 'http://c4f.test/') return route.fulfill({ contentType: 'text/html', body: html });
|
||||
const font = url.endsWith('/fonts/arabic-light.otf') ? 'SSTArabic-Light.otf' : url.endsWith('/fonts/arabic-regular.otf') ? 'SSTArabic-Roman.otf' : '';
|
||||
if (font && process.env.C4F_TEST_PS4_FONTS) return route.fulfill({ contentType: 'font/otf', body: fs.readFileSync(process.env.C4F_TEST_PS4_FONTS + '/' + font) });
|
||||
return route.abort();
|
||||
});
|
||||
await page.goto('http://c4f.test/');
|
||||
if (width === 1440) assert(await page.locator('.screen-sub').evaluate(e => e.getBoundingClientRect().width > 800),
|
||||
'introductory copy uses the available width instead of an English-sized character cap');
|
||||
await page.waitForFunction(() => window.c4fTest?.gamepadSources.size === 1);
|
||||
assert.equal(await page.evaluate(() => c4fTest.keymap.CROSS), 'Space', 'keyboard event codes never follow the interface language');
|
||||
if (language === 'ar') {
|
||||
// The normal CI fixture denies font endpoints: embedded offline fallback remains usable.
|
||||
// Optional local fixture uses console-supplied files without redistributing Sony bytes.
|
||||
await page.evaluate(() => c4fI18n.loadConsoleFonts('http://c4f.test/'));
|
||||
if (process.env.C4F_TEST_PS4_FONTS) assert.deepEqual(await page.evaluate(() =>
|
||||
[...document.fonts].filter(font => font.family.replaceAll(String.fromCharCode(34), '') === 'SST Arabic' && font.status === 'loaded').map(font => font.weight).sort()), ['300', '400'],
|
||||
'both console SST weights are actually loaded, rather than only available through fallback');
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await settle(page);
|
||||
}
|
||||
assert.equal(clean(await page.locator('html').getAttribute('lang')), language);
|
||||
assert.equal(clean(await page.locator('html').getAttribute('dir')), language === 'ar' ? 'rtl' : 'ltr');
|
||||
assert.equal(clean(await page.locator('#openSettings').getAttribute('aria-label')), language === 'ar' ? 'الإعدادات' : language === 'pt-BR' ? 'Configurações' : 'Settings');
|
||||
if (language === 'ar' && (width === 390 || width === 1440)) {
|
||||
fs.mkdirSync('dev/web-preview', { recursive: true });
|
||||
await page.evaluate(() => { document.querySelector('#toast').classList.remove('show'); document.querySelector('#toast').style.visibility = 'hidden'; });
|
||||
await page.screenshot({ path: 'dev/web-preview/ar-home-' + width + '.png' });
|
||||
}
|
||||
await page.click('#openSettings');
|
||||
await page.locator('#langSelect').scrollIntoViewIfNeeded();
|
||||
await settle(page);
|
||||
const bounds = await page.locator('#langSelect').evaluate(select => {
|
||||
const row = select.closest('.row'), a = select.getBoundingClientRect(), b = row.getBoundingClientRect();
|
||||
return { left: a.left, right: a.right, rowLeft: b.left, rowRight: b.right, scroll: row.scrollWidth, width: row.clientWidth };
|
||||
});
|
||||
assert(bounds.left >= bounds.rowLeft - 1 && bounds.right <= bounds.rowRight + 1, `${width}: picker fits ${JSON.stringify(bounds)}`);
|
||||
assert(bounds.scroll <= bounds.width + 1, `${width}: language row does not overflow`);
|
||||
|
||||
if (language === 'ar' && (width === 390 || width === 1440)) {
|
||||
await page.screenshot({ path: 'dev/web-preview/ar-settings-' + width + '.png' });
|
||||
for (const section of ['keyboard', 'gamepads', 'experimental']) {
|
||||
await page.click('[data-page="' + section + '"]');
|
||||
await page.screenshot({ path: 'dev/web-preview/ar-' + section + '-' + width + '.png' });
|
||||
}
|
||||
}
|
||||
// Runtime data has no localization observer, including after later mutations.
|
||||
await page.evaluate(() => {
|
||||
const box = document.createElement('div');
|
||||
box.id = 'rawFixture'; box.translate = false;
|
||||
box.innerHTML = '<span title="Left" aria-label="Settings">Left</span>';
|
||||
document.body.appendChild(box);
|
||||
});
|
||||
await settle(page);
|
||||
assert.deepEqual(await page.locator('#rawFixture span').evaluate(e => [e.textContent, e.title, e.getAttribute('aria-label')]), ['Left', 'Left', 'Settings']);
|
||||
await page.locator('#rawFixture span').evaluate(e => { e.textContent = 'Right'; e.title = 'Settings'; });
|
||||
await settle(page);
|
||||
assert.deepEqual(await page.locator('#rawFixture span').evaluate(e => [e.textContent, e.title]), ['Right', 'Settings']);
|
||||
|
||||
for (const name of ['Left', 'Esquerda', 'Left. Right.', '<b>Settings</b>', 'أحمد DS4']) {
|
||||
await page.evaluate(name => {
|
||||
const t = c4fTest, src = t.gamepadSources.get(0);
|
||||
mockPad.id = name; src.signature.id = name;
|
||||
const profile = t.ControllerMapping.browserDefault(src.signature);
|
||||
profile.id = 'custom'; profile.name = name;
|
||||
t.profileStore.profiles = [profile]; src.profileId = profile.id;
|
||||
Object.assign(t.status[0], { known: true, user: name, state: 'ready', open: true, connected: true });
|
||||
t.renderPads(); t.renderGamepads(); t.setPrimary(0); t.renderPadLabel();
|
||||
t.mapper.open(0);
|
||||
}, name);
|
||||
await settle(page);
|
||||
if (language === 'ar' && width === 1440 && name === 'أحمد DS4') await page.screenshot({path:'dev/web-preview/ar-mapping-desktop.png'});
|
||||
assert.equal(clean(await page.locator('.tile-name').first().textContent()), name);
|
||||
assert.equal(clean(await page.locator('#gamepadList .gp-name').first().textContent()), name);
|
||||
assert.equal(clean(await page.locator('#gamepadList .gp-profile').first().textContent()), name);
|
||||
assert.equal(clean(await page.locator('#mapProfile option[value="custom"]').textContent()), name);
|
||||
assert.equal(clean(await page.locator('#mapSource option[value="0"]').textContent()), gamepad + ' 1 · ' + name);
|
||||
assert.equal(clean(await page.locator('#gamepadList [data-focus="pad-0"]').getAttribute('title')), controller + ' 1 · ' + name);
|
||||
assert(clean(await page.locator('#padLabel').textContent()).startsWith(controller + ' 1 · ' + name));
|
||||
await page.evaluate(name => {
|
||||
c4fTest.mapper.draft.name = name;
|
||||
document.querySelector('#mapName').value = name;
|
||||
c4fTest.mapper.save(true);
|
||||
}, name);
|
||||
await settle(page);
|
||||
assert.equal(clean(await page.locator('#mapStatus').textContent()), language === 'ar' ? 'حُفظ باسم ' + name + '.' : language === 'pt-BR' ? 'Salvo como ' + name + '.' : 'Saved as ' + name + '.');
|
||||
await page.evaluate(() => c4fTest.mapper.remove());
|
||||
await settle(page);
|
||||
assert.equal(clean(await page.locator('#mapStatus').textContent()), language === 'ar' ? 'اضغط «حذف» مجددًا لحذف ' + name + '.' : language === 'pt-BR' ? 'Aperte Excluir de novo para excluir ' + name + '.' : 'Press Delete again to delete ' + name + '.');
|
||||
await page.evaluate(() => { c4fTest.mapper.backArmed = 0; c4fTest.mapper.deleteArmed = 0; c4fTest.mapper.close(); });
|
||||
}
|
||||
const mapping = await page.evaluate(() => {
|
||||
const m = c4fTest.mapper;
|
||||
m.open(0);
|
||||
m.draft = c4fTest.ControllerMapping.browserDefault(c4fTest.gamepadSources.get(0).signature);
|
||||
m.draft.bindings.find(binding => binding.output === 'lx').invert = true;
|
||||
m.target = 'LS'; m.render();
|
||||
const reversed = document.querySelector('#mapPanel .map-row-label').textContent;
|
||||
m.draft.calibration[1] = { type: 'pedal', released: -1, full: 1 };
|
||||
m.tab = 'inputs'; m.render();
|
||||
const pedal = document.querySelector('#mapInputs [data-input="axis:1"] .in-name').textContent;
|
||||
m.openInput({kind: 'button', index: 0});
|
||||
const output = document.querySelector('#mapPanel .map-section h4').textContent;
|
||||
const type = m.draft.calibration[1].type;
|
||||
m.close();
|
||||
return { reversed, pedal, output, type };
|
||||
});
|
||||
assert(mapping.reversed.includes(language === 'ar' ? 'معكوس' : language === 'pt-BR' ? 'invertido' : 'reversed'));
|
||||
assert(mapping.pedal.includes(language === 'ar' ? 'دواسة' : 'pedal'));
|
||||
assert.equal(mapping.output, language === 'ar' ? '×' : language === 'pt-BR' ? 'X' : 'Cross');
|
||||
assert.equal(mapping.type, 'pedal', 'calibration machine values are not translated');
|
||||
// Closed and open drawers must each use the proper RTL side and transform.
|
||||
const drawer = page.locator('#optionsMenu');
|
||||
await drawer.evaluate(element => { element.style.transition = 'none'; });
|
||||
const closed = await drawer.boundingBox();
|
||||
if (language === 'ar') assert(closed.x + closed.width <= 1, 'RTL drawer is hidden beyond the left edge');
|
||||
else assert(closed.x >= width - 1, 'LTR drawer is hidden beyond the right edge');
|
||||
await page.locator('#padMenuBtn').evaluate(button => button.click());
|
||||
const open = await drawer.boundingBox();
|
||||
assert(open.x >= -1 && open.x + open.width <= width + 1, 'open drawer fits the viewport');
|
||||
assert(Math.abs(open.x - (language === 'ar' ? 0 : width - open.width)) < 1, 'drawer follows the interface direction');
|
||||
await page.locator('#closeOptions').evaluate(button => button.click());
|
||||
assert.equal(await page.locator('#mapPad .pad-area').evaluate(e => getComputedStyle(e).direction), 'ltr');
|
||||
assert.deepEqual(errors, [], `${width}: page errors`);
|
||||
if (width === 1440) {
|
||||
for (const choice of ['ar', 'pt-BR', 'en', 'auto']) {
|
||||
await page.locator('#langSelect').evaluate((select, value) => { select.value = value; select.dispatchEvent(new Event('change')); }, choice);
|
||||
const expected = choice === 'auto' ? language : choice;
|
||||
await page.waitForFunction(expected => document.documentElement.lang === expected, expected);
|
||||
assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem('c4f.lang'))), choice);
|
||||
}
|
||||
}
|
||||
await context.close();
|
||||
console.log(`PASS ${language} ${width}px: raw names, options, tooltips, messages, explicit boundaries and language picker`);
|
||||
}
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,77 @@
|
||||
// Exercise the shipped offline message catalogue and language preference logic.
|
||||
const fs = require('node:fs'), vm = require('node:vm'), assert = require('node:assert/strict');
|
||||
const page = fs.readFileSync('client/index.html', 'utf8');
|
||||
const from = page.indexOf(' // Interface language.');
|
||||
const end = page.indexOf('\n }());', from) + '\n }());'.length;
|
||||
assert(from > 0 && end > from);
|
||||
const source = page.slice(from,end);
|
||||
assert(!source.includes('MutationObserver'), 'interface localization must not inspect arbitrary runtime data');
|
||||
function load({languages = ['en-US'], saved = null} = {}) {
|
||||
const ctx = { navigator: {languages, language: languages[0]},
|
||||
localStorage: {getItem: () => saved, setItem() {}},
|
||||
document: { documentElement: {}, querySelectorAll: () => [], getElementById: () => null } };
|
||||
ctx.window = ctx;
|
||||
vm.createContext(ctx); vm.runInContext(source,ctx);
|
||||
return ctx;
|
||||
}
|
||||
for (const [languages,saved,expected] of [
|
||||
[['en-US'],null,'en'], [['pt-PT'],null,'pt-BR'], [['ar-SA'],null,'ar'],
|
||||
[['en-GB','ar'],null,'en'], [['ar'],'"en"','en'], [['en'],'"pt-BR"','pt-BR'],
|
||||
[['en'],'"ar"','ar'], [['ar'],'"auto"','ar'], [['ar'],'"unsupported"','ar'],
|
||||
[['ar'],'invalid json','ar']
|
||||
]) assert.equal(load({languages,saved}).c4fLang,expected);
|
||||
const en=load(), pt=load({languages:['pt-BR']}), ar=load({languages:['ar']});
|
||||
assert.equal(en.document.documentElement.dir,'ltr');
|
||||
assert.equal(ar.document.documentElement.dir,'rtl');
|
||||
assert.equal(pt.c4fI18n.t('Settings'),'Configurações');
|
||||
assert.equal(ar.c4fI18n.t('Settings'),'الإعدادات');
|
||||
assert.equal(ar.c4fI18n.t('Controller {number}',{number:2}),'يد التحكم \u20682\u2069');
|
||||
for (const ctx of [en,pt,ar]) {
|
||||
const {t,messages}=ctx.c4fI18n;
|
||||
for (const name of ['Left','Esquerda','Left. Right.','Settings','أحمد DS4','<b>Settings</b>']) {
|
||||
assert(t('Saved as {name}.',{name}).includes(name), 'inserted names stay unchanged');
|
||||
assert.equal(t('Unknown service error 173'),'Unknown service error 173');
|
||||
}
|
||||
for (const key of Object.keys(messages['pt-BR'])) {
|
||||
assert(messages.ar[key], 'missing Arabic: '+key);
|
||||
for (const locale of ['pt-BR','ar']) assert.deepEqual(
|
||||
(messages[locale][key].match(/\{\w+\}/g)||[]).sort(),
|
||||
(key.match(/\{\w+\}/g)||[]).sort(), 'template values: '+key);
|
||||
}
|
||||
}
|
||||
for (const match of page.slice(0,from).matchAll(/data-i18n(?:-(?:aria-label|title|placeholder))?="([^"]+)"/g))
|
||||
assert(ar.c4fI18n.messages.ar[match[1].replace(/"/g,'"').replace(/&/g,'&')], 'markup key: '+match[1]);
|
||||
assert(!/[\u0660-\u0669\u06f0-\u06f9]/.test(JSON.stringify(ar.c4fI18n.messages.ar)), 'Arabic uses 0–9');
|
||||
assert.equal((page.match(/data:font\/ttf;base64,/g)||[]).length,2, 'both Arabic weights work offline');
|
||||
console.log('PASS offline English/Portuguese/Arabic catalogues, template boundaries, preferences, RTL and embedded fonts');
|
||||
|
||||
// A partial SST failure must retry only the missing weight, retaining the loaded one.
|
||||
(async () => {
|
||||
const ctx = load({ languages: ['ar'] }), faces = new Set(), created = [], attempts = {};
|
||||
ctx.URL = URL;
|
||||
ctx.document.fonts = { add: face => faces.add(face), delete: face => faces.delete(face) };
|
||||
ctx.FontFace = class {
|
||||
constructor(family, source, options) {
|
||||
Object.assign(this, options, { family, source, status: 'unloaded' }); created.push(this);
|
||||
}
|
||||
load() {
|
||||
attempts[this.weight] = (attempts[this.weight] || 0) + 1;
|
||||
if (this.weight === '400' && attempts[this.weight] === 1) return Promise.reject(Error('temporary font failure'));
|
||||
this.status = 'loaded'; return Promise.resolve(this);
|
||||
}
|
||||
};
|
||||
await ctx.c4fI18n.loadConsoleFonts('http://c4f.test/');
|
||||
assert.equal(faces.size, 1);
|
||||
const retained = [...faces][0];
|
||||
await ctx.c4fI18n.loadConsoleFonts('http://c4f.test/');
|
||||
assert.equal(faces.size, 2);
|
||||
assert(faces.has(retained), 'the successfully loaded weight remains during retry');
|
||||
assert.deepEqual(attempts, { 300: 1, 400: 2 });
|
||||
await ctx.c4fI18n.loadConsoleFonts('http://c4f.test/');
|
||||
assert.equal(created.length, 3, 'complete font sets are not requested again');
|
||||
await ctx.c4fI18n.loadConsoleFonts('http://second-c4f.test/');
|
||||
assert.equal(faces.size, 2, 'changing consoles replaces both old faces without growth');
|
||||
assert(!faces.has(retained));
|
||||
assert([...faces].every(face => face.source.includes('http://second-c4f.test/fonts/')));
|
||||
console.log('PASS missing SST weight retries, loaded weight retention and bounded font replacement');
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Host render fixtures start in English unless they explicitly choose a locale. */
|
||||
int sceSystemServiceParamGetInt(int id, int *value)
|
||||
{
|
||||
(void)id;
|
||||
*value = 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -550,6 +550,30 @@ static void diagnosticNavigation(void)
|
||||
puts("PASS diagnostic arrows remain page/scroll inputs outside Actions; remote can choose Capture");
|
||||
}
|
||||
|
||||
static void languageButton(void)
|
||||
{
|
||||
C4fLauncherScreen screen = stopped();
|
||||
uint32_t standardUnhandled, remoteUnhandled;
|
||||
assert(c4fNavigate(&screen, C4F_INPUT_BUTTON_L1, 0, 0, &standardUnhandled, &remoteUnhandled) == C4F_ACTION_LANGUAGE);
|
||||
assert(!standardUnhandled && !remoteUnhandled);
|
||||
/* Even while the app is busy: it changes nothing on the console. */
|
||||
screen.busy = 1;
|
||||
assert(navigate(&screen, C4F_INPUT_BUTTON_L1, 0, 0) == C4F_ACTION_LANGUAGE);
|
||||
screen.busy = 0;
|
||||
/* Open menus and the stop confirmation keep L1 to themselves. */
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(screen.actionMenu && navigate(&screen, C4F_INPUT_BUTTON_L1, 0, 0) == C4F_ACTION_NONE);
|
||||
screen = stopped();
|
||||
screen.running = 1;
|
||||
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
|
||||
assert(screen.confirmStop && navigate(&screen, C4F_INPUT_BUTTON_L1, 0, 0) == C4F_ACTION_NONE && screen.confirmStop);
|
||||
/* The diagnostic build pages with L1, so it is left unhandled there. */
|
||||
screen = stopped();
|
||||
assert(c4fNavigate(&screen, C4F_INPUT_BUTTON_L1, 0, 1, &standardUnhandled, &remoteUnhandled) == C4F_ACTION_NONE);
|
||||
assert(standardUnhandled == C4F_INPUT_BUTTON_L1);
|
||||
puts("PASS L1 changes the language on the main screen only, and still pages the diagnostic build");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
remoteKeyNormalization();
|
||||
@@ -565,5 +589,6 @@ int main(void)
|
||||
stopConfirmation();
|
||||
unavailableActions();
|
||||
diagnosticNavigation();
|
||||
languageButton();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* Reuse the real tester fixtures; compare fresh and language-switched frames. */
|
||||
#include "draw.h"
|
||||
#define main c4fOriginalRendererMain
|
||||
#include "tester_render_test.c"
|
||||
#undef main
|
||||
#include "language.h"
|
||||
#include <assert.h>
|
||||
|
||||
int sceSystemServiceParamGetInt(int id, int *value)
|
||||
{
|
||||
(void)id;
|
||||
*value = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int c4fRawNameSeen;
|
||||
static const char *c4fNameGuard(const char *text)
|
||||
{
|
||||
if (!strcmp(text, "RawProfileNameMarker")) c4fRawNameSeen++;
|
||||
return c4fPortugueseText(text);
|
||||
}
|
||||
|
||||
static void c4fMetadataFixture(C4fLauncherScreen *screen, int which)
|
||||
{
|
||||
c4fFixture(screen, c4fCaseId("menu"));
|
||||
if (which < 2) {
|
||||
/* A real username may equal the English fallback. Only the generated
|
||||
* flag distinguishes whether that text is translated. */
|
||||
snprintf(screen->tester.user[0].name, sizeof(screen->tester.user[0].name), "User 1");
|
||||
screen->tester.user[0].nameGenerated = which;
|
||||
} else {
|
||||
/* The English diagnostic is identical, while explicit argument
|
||||
* provenance controls whether this value is an interface key. */
|
||||
c4fMessageWrite(&screen->tester.noteText, screen->tester.note,
|
||||
sizeof(screen->tester.note), "The app cannot reach Control4Free: %s", "Left");
|
||||
screen->tester.noteText.valueIsKey = which % 2;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
assert(!c4fScreenInit());
|
||||
uint32_t *pixels = calloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT, sizeof(*pixels));
|
||||
assert(pixels);
|
||||
C4fLauncherScreen screen;
|
||||
if (argc == 2 && !strcmp(argv[1], "names")) {
|
||||
c4fSetTextTranslation(c4fNameGuard);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
c4fFixture(&screen, c4fCaseId(i == 0 ? "idle" : i == 1 ? "menu" : "color-red"));
|
||||
snprintf(screen.tester.user[0].name, sizeof(screen.tester.user[0].name), "RawProfileNameMarker");
|
||||
c4fDrawTester(pixels, &screen);
|
||||
}
|
||||
assert(!c4fRawNameSeen);
|
||||
assert(c4fTextWidthRaw(C4F_FONT_REGULAR, 26, "Left") != c4fTextWidth(C4F_FONT_REGULAR, 26, "Left"));
|
||||
puts("PASS names bypass interface translation in tabs, selected user and touchpad");
|
||||
} else if (argc == 5 && !strcmp(argv[1], "metadata")) {
|
||||
c4fLanguageUse(!strcmp(argv[2], "ar") ? C4F_LANGUAGE_ARABIC : C4F_LANGUAGE_PORTUGUESE);
|
||||
int which = atoi(argv[4]);
|
||||
assert(which >= 0 && which < 4);
|
||||
if (!strcmp(argv[3], "warm")) {
|
||||
c4fMetadataFixture(&screen, which ^ 1);
|
||||
c4fDrawTester(pixels, &screen);
|
||||
}
|
||||
c4fMetadataFixture(&screen, which);
|
||||
c4fDrawTester(pixels, &screen);
|
||||
printf("%016llx\n", (unsigned long long)c4fHash(pixels));
|
||||
} else {
|
||||
assert(argc == 4);
|
||||
const int language = !strcmp(argv[1], "pt") ? C4F_LANGUAGE_PORTUGUESE :
|
||||
!strcmp(argv[1], "ar") ? C4F_LANGUAGE_ARABIC : C4F_LANGUAGE_ENGLISH;
|
||||
if (!strcmp(argv[2], "warm")) {
|
||||
c4fLanguageUse(language == C4F_LANGUAGE_ENGLISH ? C4F_LANGUAGE_PORTUGUESE : C4F_LANGUAGE_ENGLISH);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
c4fFixture(&screen, c4fCaseId("color-red"));
|
||||
screen.tester.colorChannel = i % 3;
|
||||
screen.tester.colorFine = i / 3;
|
||||
c4fDrawTester(pixels, &screen);
|
||||
}
|
||||
}
|
||||
c4fLanguageUse(language);
|
||||
c4fFixture(&screen, c4fCaseId("color-red"));
|
||||
int which = atoi(argv[3]);
|
||||
screen.tester.colorChannel = which % 3;
|
||||
screen.tester.colorFine = which / 3;
|
||||
c4fDrawTester(pixels, &screen);
|
||||
printf("%016llx\n", (unsigned long long)c4fHash(pixels));
|
||||
}
|
||||
free(pixels);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/* Exact offline interface catalogs, formatting, and automatic language choice.
|
||||
* The SDK's system language call and renderer hooks are replaced. */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../launcher/language.c"
|
||||
|
||||
static const char *(*shownWith)(const char *text);
|
||||
static unsigned changes;
|
||||
static int systemLanguage = 1, systemError;
|
||||
static int rtl;
|
||||
|
||||
void c4fSetTextDirection(int value) { rtl = value; }
|
||||
|
||||
void c4fSetTextTranslation(const char *(*translate)(const char *text))
|
||||
{
|
||||
shownWith = translate;
|
||||
changes++;
|
||||
}
|
||||
|
||||
unsigned c4fTextTranslationGeneration(void) { return changes; }
|
||||
|
||||
int sceSystemServiceParamGetInt(int id, int *value)
|
||||
{
|
||||
assert(id == C4F_SYSTEM_PARAM_LANG);
|
||||
*value = systemLanguage;
|
||||
return systemError;
|
||||
}
|
||||
|
||||
/* draw.c shows two-byte UTF-8 as Latin-1 and anything longer as '?'. */
|
||||
static int drawable(const char *text)
|
||||
{
|
||||
for (const unsigned char *s = (const unsigned char *)text; *s; s++) {
|
||||
if (*s < 0x80) { if (*s < 32 || *s == 127) return 0; continue; }
|
||||
if (s[0] != 0xc2 && s[0] != 0xc3) return 0;
|
||||
if ((s[1] & 0xc0) != 0x80 || (s[0] == 0xc2 && s[1] < 0xa0)) return 0;
|
||||
s++;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The conversions in order, as one string ("%d%s" for "%d of %s"). */
|
||||
static void conversions(const char *text, char *out, size_t size)
|
||||
{
|
||||
size_t used = 0;
|
||||
for (const char *at = text; *at; at++) {
|
||||
if (*at != '%') continue;
|
||||
const char *end = at + 1;
|
||||
while (*end && strchr("0123456789.-+ #lhz", *end)) end++;
|
||||
if (*end) end++;
|
||||
size_t length = (size_t)(end - at);
|
||||
assert(used + length < size);
|
||||
memcpy(out + used, at, length);
|
||||
used += length;
|
||||
at = end - 1;
|
||||
}
|
||||
out[used] = 0;
|
||||
}
|
||||
|
||||
static void table(void)
|
||||
{
|
||||
const size_t count = sizeof(c4fPortuguese) / sizeof(c4fPortuguese[0]);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const C4fPhrase *p = &c4fPortuguese[i];
|
||||
char en[32], pt[32];
|
||||
assert(drawable(p->pt));
|
||||
conversions(p->en, en, sizeof(en));
|
||||
conversions(p->pt, pt, sizeof(pt));
|
||||
if (strcmp(en, pt)) { fprintf(stderr, "conversions differ: %s\n", p->en); assert(0); }
|
||||
for (size_t j = 0; j < i; j++) assert(strcmp(p->en, c4fPortuguese[j].en));
|
||||
assert(c4fPortugueseText(p->en) == p->pt);
|
||||
}
|
||||
printf("PASS %zu Portuguese texts: drawable, unique, same filled-in values\n", count);
|
||||
}
|
||||
|
||||
static void formattedKeys(void)
|
||||
{
|
||||
assert(!strcmp(c4fPortugueseText("Close"), "Fechar"));
|
||||
assert(!strcmp(c4fPortugueseText("1 controller connected"), "1 controle conectado"));
|
||||
char out[512];
|
||||
snprintf(out, sizeof(out), c4fPortugueseText("%d controllers connected"), 3);
|
||||
assert(!strcmp(out, "3 controles conectados"));
|
||||
snprintf(out, sizeof(out), c4fPortugueseText("Cannot read (0x%08x)"), 0x80020001u);
|
||||
assert(!strcmp(out, "Não foi possível ler (0x80020001)"));
|
||||
/* Translate the template before inserting a literal value. A value that
|
||||
* happens to be an interface key must stay literal. */
|
||||
snprintf(out, sizeof(out), c4fPortugueseText("Sent, but Control4Free did not start (%s). Retry Start."), "Left");
|
||||
assert(!strcmp(out, "Enviado, mas o Control4Free não iniciou (Left). Tente Iniciar de novo."));
|
||||
assert(!c4fPortugueseText("3 controllers connected"));
|
||||
assert(!c4fPortugueseText("User 1"));
|
||||
assert(!c4fPortugueseText("Cannot read (0x80020001)"));
|
||||
/* Unknown keys and near misses remain untouched. */
|
||||
assert(!c4fPortugueseText("Lucas") && !c4fPortugueseText("") && !c4fPortugueseText(NULL));
|
||||
assert(!c4fPortugueseText(" controllers connected"));
|
||||
assert(!c4fPortugueseText("Close "));
|
||||
puts("PASS exact keys, formatting before value insertion, no filled-string matching");
|
||||
}
|
||||
|
||||
static void arabicTable(void)
|
||||
{
|
||||
const size_t count = sizeof(c4fArabic) / sizeof(c4fArabic[0]);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const C4fPhrase *p = &c4fArabic[i];
|
||||
char en[32], ar[32];
|
||||
conversions(p->en, en, sizeof(en));
|
||||
conversions(p->pt, ar, sizeof(ar));
|
||||
assert(!strcmp(en, ar));
|
||||
assert(c4fArabicText(p->en) == p->pt);
|
||||
for (size_t j = 0; j < i; ++j) assert(strcmp(p->en, c4fArabic[j].en));
|
||||
for (const unsigned char *s = (const unsigned char *)p->pt; *s; ++s)
|
||||
assert(!(s[0] == 0xd9 && s[1] >= 0xa0 && s[1] <= 0xa9)); /* keep digits 0-9 */
|
||||
}
|
||||
assert(count >= sizeof(c4fPortuguese) / sizeof(c4fPortuguese[0]));
|
||||
assert(!c4fArabicText("User 1") && !c4fArabicText(NULL));
|
||||
puts("PASS Arabic catalog: unique keys, matching format parameters, digits 0-9");
|
||||
}
|
||||
|
||||
static void choice(void)
|
||||
{
|
||||
systemLanguage = 1; /* English (United States) */
|
||||
c4fLanguageUse(C4F_LANGUAGE_AUTO);
|
||||
assert(!shownWith);
|
||||
unsigned before = changes;
|
||||
c4fLanguageUse(C4F_LANGUAGE_ENGLISH);
|
||||
assert(!shownWith && changes == before);
|
||||
c4fLanguageUse(C4F_LANGUAGE_PORTUGUESE);
|
||||
assert(shownWith == c4fPortugueseText && !rtl);
|
||||
assert(!strcmp(c4fTr("Left"), "Esquerdo"));
|
||||
assert(!strcmp(c4fTr("Unknown service error"), "Unknown service error"));
|
||||
c4fLanguageUse(C4F_LANGUAGE_ARABIC);
|
||||
assert(shownWith == c4fArabicText && rtl && c4fLanguageIsArabic());
|
||||
assert(!strcmp(c4fTr("Red"), "أحمر"));
|
||||
systemLanguage = 21;
|
||||
c4fLanguageUse(C4F_LANGUAGE_ENGLISH);
|
||||
assert(!shownWith && !rtl && !c4fLanguageIsArabic());
|
||||
c4fLanguageUse(C4F_LANGUAGE_AUTO);
|
||||
assert(shownWith == c4fArabicText && rtl);
|
||||
c4fLanguageUse(C4F_LANGUAGE_ENGLISH);
|
||||
assert(!shownWith);
|
||||
for (int language = 7; language <= 17; language += 10) { /* Portuguese, then Portuguese (Brazil) */
|
||||
systemLanguage = language;
|
||||
c4fLanguageUse(C4F_LANGUAGE_AUTO);
|
||||
assert(shownWith == c4fPortugueseText);
|
||||
c4fLanguageUse(C4F_LANGUAGE_ENGLISH);
|
||||
}
|
||||
systemLanguage = 17; systemError = -1;
|
||||
c4fLanguageUse(C4F_LANGUAGE_AUTO);
|
||||
assert(!shownWith);
|
||||
puts("PASS automatic follows PS4 Portuguese/Arabic; explicit choices override it");
|
||||
}
|
||||
|
||||
static void workerMessages(void)
|
||||
{
|
||||
char english[160], out[C4F_MESSAGE_RENDER_SIZE];
|
||||
C4fMessage message;
|
||||
c4fMessageWrite(&message, english, sizeof(english),
|
||||
"Sent, but Control4Free did not start (%s). Retry Start.", "Left");
|
||||
c4fLanguageUse(C4F_LANGUAGE_PORTUGUESE);
|
||||
c4fMessageRender(&message, out, sizeof(out));
|
||||
assert(strstr(out, "(Left)") && !strstr(out, "Esquerdo"));
|
||||
c4fLanguageUse(C4F_LANGUAGE_ARABIC);
|
||||
c4fMessageRender(&message, out, sizeof(out));
|
||||
assert(strstr(out, "\xE2\x81\xA8Left\xE2\x81\xA9"));
|
||||
assert(!strcmp(english, "Sent, but Control4Free did not start (Left). Retry Start."));
|
||||
c4fMessageWrite(&message, english, sizeof(english), "The PS4 refused that (0x%08x).", 0x80020001u);
|
||||
c4fMessageRender(&message, out, sizeof(out));
|
||||
assert(strstr(out, "0x80020001"));
|
||||
c4fLanguageUse(C4F_LANGUAGE_ENGLISH);
|
||||
c4fMessageRender(&message, out, sizeof(out));
|
||||
assert(!strcmp(out, english));
|
||||
puts("PASS worker messages: language switching, literal values, Arabic isolation, English diagnostics");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
table();
|
||||
formattedKeys();
|
||||
arabicTable();
|
||||
choice();
|
||||
workerMessages();
|
||||
return 0;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ typedef struct {
|
||||
int count, listError;
|
||||
FakePad pads[4];
|
||||
int stateCalls;
|
||||
const char *nameOverride;
|
||||
} Fake;
|
||||
|
||||
static FakePad *byUser(Fake *f, int32_t user)
|
||||
@@ -52,7 +53,11 @@ static int fakeUsers(void *context, int32_t *users)
|
||||
|
||||
static void fakeName(void *context, int32_t user, char *name, int size)
|
||||
{
|
||||
(void)context;
|
||||
Fake *f = context;
|
||||
if (user == 1 && f->nameOverride) {
|
||||
snprintf(name, (size_t)size, "%s", f->nameOverride);
|
||||
return;
|
||||
}
|
||||
/* User 3 has no name the app can draw: the tester falls back. */
|
||||
if (user == 3) { name[0] = 0; return; }
|
||||
snprintf(name, (size_t)size, user == 1 ? "Alex" : user == 2 ? "Sam\x01" : "Kim");
|
||||
@@ -447,16 +452,20 @@ static void gridNavigationAndRepeat(void)
|
||||
{ 4, 4, 3, 1 }, { 5, 5, 0, 2 }, { 6, 6, 1, 3 }, { 7, 7, 2, 0 },
|
||||
{ 0, 0, 7, 5 }, { 1, 1, 4, 6 }, { 2, 2, 5, 7 }, { 3, 3, 6, 4 }
|
||||
};
|
||||
for (int item = 0; item < C4F_TESTER_ITEMS; item++) {
|
||||
for (int direction = 0; direction < 4; direction++) {
|
||||
t.focus = item;
|
||||
tap(&f, &t, &now, directions[direction]);
|
||||
assert(t.focus == neighbors[item][direction]);
|
||||
t.focus = item;
|
||||
frame(&f, &t, now += 10, 0, directions[direction]);
|
||||
assert(t.focus == neighbors[item][direction]);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
assert(t.focus == neighbors[item][direction]);
|
||||
for (int rtl = 0; rtl < 2; rtl++) {
|
||||
t.rtl = rtl;
|
||||
for (int item = 0; item < C4F_TESTER_ITEMS; item++) {
|
||||
for (int direction = 0; direction < 4; direction++) {
|
||||
int visibleDirection = rtl && direction >= 2 ? 5 - direction : direction;
|
||||
t.focus = item;
|
||||
tap(&f, &t, &now, directions[direction]);
|
||||
assert(t.focus == neighbors[item][visibleDirection]);
|
||||
t.focus = item;
|
||||
frame(&f, &t, now += 10, 0, directions[direction]);
|
||||
assert(t.focus == neighbors[item][visibleDirection]);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
assert(t.focus == neighbors[item][visibleDirection]);
|
||||
}
|
||||
}
|
||||
}
|
||||
c4fTesterSuspend(&t, now + 1000);
|
||||
@@ -466,11 +475,12 @@ static void gridNavigationAndRepeat(void)
|
||||
puts("PASS 4-by-2 spatial wrap for controller/TV remote, immediate taps, 350/100 ms hold repeat and reset on focus loss");
|
||||
}
|
||||
|
||||
static void colorPicker(void)
|
||||
static void colorPicker(int rtl)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
t.rtl = rtl;
|
||||
uint64_t now = openMenu(&f, &t);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
t.focus = C4F_TESTER_LIGHT_COLOR;
|
||||
@@ -741,15 +751,32 @@ static void launcherEntry(void)
|
||||
puts("PASS Test controllers: in Actions unless locked, Options opens it, every menu item has a label");
|
||||
}
|
||||
|
||||
static void unicodeNames(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
const char *names[] = { "João", "أحمد DS4", "سام (Sam) 123", "أحمدمحمدأحمدمحمد" };
|
||||
for (unsigned i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
|
||||
setup(&f, &t); f.nameOverride = names[i];
|
||||
c4fTesterOpen(&t, 0);
|
||||
C4fTesterView view; c4fTesterView(&t, &view);
|
||||
assert(!strcmp(view.user[0].name, names[i]));
|
||||
c4fTesterClose(&t);
|
||||
}
|
||||
puts("PASS Portuguese, Arabic and mixed UTF-8 usernames survive the input reader unchanged");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
holdTiming();
|
||||
readsUsers();
|
||||
unicodeNames();
|
||||
menuActions();
|
||||
switchUsers();
|
||||
menuOpeningSafety();
|
||||
gridNavigationAndRepeat();
|
||||
colorPicker();
|
||||
colorPicker(0);
|
||||
colorPicker(1);
|
||||
menuAvailability();
|
||||
failures();
|
||||
logout();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/* Exercise the shipped Unicode layout/cache internals as well as public
|
||||
* measurement and drawing; the console font may be supplied by the harness. */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include "../launcher/text.c"
|
||||
|
||||
static uint32_t pixels[800 * 300];
|
||||
static C4fCanvas canvas = { pixels, 800, 300 };
|
||||
static const C4fColor white = { 255, 255, 255, 255 };
|
||||
|
||||
static uint64_t hash(void)
|
||||
{
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
for (size_t i = 0; i < sizeof(pixels) / sizeof(pixels[0]); i++) { h ^= pixels[i]; h *= 1099511628211ULL; }
|
||||
return h;
|
||||
}
|
||||
|
||||
static void draw(const char *text)
|
||||
{
|
||||
memset(pixels, 0, sizeof(pixels));
|
||||
float width = c4fTextWidthRaw(C4F_FONT_REGULAR, 36, text);
|
||||
float drawn = c4fTextRaw(&canvas, C4F_FONT_REGULAR, 36, 100, 120, white, text);
|
||||
assert(width > 0 && fabsf(width - drawn) < .01f);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
assert(c4fDrawInit() == 0);
|
||||
assert(c4fTextSetArabicFonts((const unsigned char *)"invalid", 7, (const unsigned char *)"invalid", 7) == -1);
|
||||
if (argc == 3) {
|
||||
size_t lightSize = 0, regularSize = 0;
|
||||
unsigned char *light = c4fReadFont(argv[1], &lightSize), *regular = c4fReadFont(argv[2], ®ularSize);
|
||||
assert(light && regular);
|
||||
draw("مرحبا");
|
||||
unsigned generation = c4fTextTranslationGeneration();
|
||||
assert(c4fTextSetArabicFonts(light, lightSize, regular, regularSize) == 0);
|
||||
assert(c4fTextTranslationGeneration() != generation && c4fRasterBytes == 0);
|
||||
/* The bytes stay alive until process exit, just as in the launcher. */
|
||||
} else if (argc == 2 && !strcmp(argv[1], "--system")) assert(!c4fTextLoadSystemFonts());
|
||||
else { assert(argc == 1); assert(c4fTextLoadSystemFonts() == -1); }
|
||||
const char *technical = "Installed dev; 192.168.1.20:4264";
|
||||
assert(!c4fTextNeedsShaping(technical, (int)strlen(technical)));
|
||||
assert(c4fTextNeedsShaping("مرحبا", (int)strlen("مرحبا")));
|
||||
C4fTextLayout *joined = c4fShape(C4F_FONT_REGULAR, 36, "ببب", (int)strlen("ببب"));
|
||||
assert(joined && joined->count >= 3 && joined->width > 0);
|
||||
unsigned nominal = (unsigned)stbtt_FindGlyphIndex(&c4fArabicSTB[C4F_FONT_REGULAR], 0x0628);
|
||||
int contextual = 0;
|
||||
for (int i = 0; i < joined->count; i++) {
|
||||
assert(joined->glyphs[i].id);
|
||||
contextual |= joined->glyphs[i].id != nominal;
|
||||
}
|
||||
assert(contextual); /* Arabic joining actually selects contextual glyph IDs. */
|
||||
C4fTextLayout *marks = c4fShape(C4F_FONT_REGULAR, 36, "مَرْحَبًا", (int)strlen("مَرْحَبًا"));
|
||||
assert(marks && marks->count > 5);
|
||||
int positioned = 0;
|
||||
for (int i = 0; i < marks->count; i++) positioned |= marks->glyphs[i].y != 0;
|
||||
assert(positioned); /* Diacritics use shaping offsets rather than spacing. */
|
||||
|
||||
const char *mixed = "مرحبا \xE2\x81\xA8" "192.168.1.20" "\xE2\x81\xA9";
|
||||
C4fTextLayout *layout = c4fShape(C4F_FONT_REGULAR, 36, mixed, (int)strlen(mixed));
|
||||
assert(layout);
|
||||
unsigned one = (unsigned)stbtt_FindGlyphIndex(&c4fArabicSTB[C4F_FONT_REGULAR], '1');
|
||||
unsigned nine = (unsigned)stbtt_FindGlyphIndex(&c4fArabicSTB[C4F_FONT_REGULAR], '9');
|
||||
unsigned two = (unsigned)stbtt_FindGlyphIndex(&c4fArabicSTB[C4F_FONT_REGULAR], '2');
|
||||
int digits = 0;
|
||||
for (int i = 0; i + 2 < layout->count; i++)
|
||||
digits |= layout->glyphs[i].id == one && layout->glyphs[i + 1].id == nine && layout->glyphs[i + 2].id == two;
|
||||
assert(digits); /* The IP's digit sequence stays LTR inside Arabic. */
|
||||
draw(mixed); uint64_t original = hash();
|
||||
for (int i = 0; i < 600; i++) {
|
||||
char sample[128]; snprintf(sample, sizeof(sample), "المستخدم Alex (%d)", i);
|
||||
c4fTextRaw(&canvas, i & 1, (float)(12 + i % 110), 50, 150, white, sample);
|
||||
assert(c4fRasterBytes <= C4F_RASTER_BYTES);
|
||||
}
|
||||
draw(mixed); assert(hash() == original); /* Cache eviction cannot change layout/raster results. */
|
||||
c4fSetTextDirection(1);
|
||||
memset(pixels, 0, sizeof(pixels));
|
||||
assert(c4fTextWrapped(&canvas, C4F_FONT_REGULAR, 36, 100, 120, 500, 60, 1, white, "مرحبا") == 1);
|
||||
int left = 800, right = -1;
|
||||
for (int y = 0; y < canvas.h; y++) for (int x = 0; x < canvas.w; x++) if (pixels[y * canvas.w + x]) {
|
||||
if (x < left) left = x;
|
||||
if (x > right) right = x;
|
||||
}
|
||||
assert(left > 350 && right <= 610);
|
||||
c4fSetTextDirection(0);
|
||||
draw("مَرْحَبًا Alex (PS4) 123");
|
||||
puts("PASS native Unicode: Arabic joining, marks, mixed-direction numbers, matching width/draw, RTL wrap, bounded cache eviction");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
// Controller layout editing through real pointer events, in an offline page.
|
||||
// Only the test's in-memory copy exposes state; the shipped page has no hooks.
|
||||
const fs = require('node:fs');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const faceButtons = ['TRIANGLE', 'CIRCLE', 'CROSS', 'SQUARE'];
|
||||
const faceSelector = button => `#padView .face-btn[data-btn="${button}"]`;
|
||||
const settle = async page => {
|
||||
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('#padView .face-btn')].every(node => getComputedStyle(node).transform === 'none'));
|
||||
};
|
||||
const snapshot = (page, root = '#padView') => page.evaluate(({ buttons, root }) => Object.fromEntries(buttons.map(button => {
|
||||
const node = document.querySelector(`${root} .face-btn[data-btn="${button}"]`);
|
||||
const r = node.getBoundingClientRect();
|
||||
return [button, { x: r.x + r.width / 2, y: r.y + r.height / 2, width: r.width, height: r.height, hidden: node.hidden }];
|
||||
})), { buttons: faceButtons, root });
|
||||
|
||||
async function drag(page, button, dx, dy) {
|
||||
const r = await page.locator(faceSelector(button)).boundingBox();
|
||||
assert(r && r.width > 0, button + ' has a visible hit target');
|
||||
await page.mouse.move(r.x + r.width / 2, r.y + r.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(r.x + r.width / 2 + dx, r.y + r.height / 2 + dy, { steps: 5 });
|
||||
await page.mouse.up();
|
||||
await settle(page);
|
||||
}
|
||||
|
||||
async function touchDrag(page, button, dx, dy) {
|
||||
const r = await page.locator(faceSelector(button)).boundingBox();
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
const x = r.x + r.width / 2, y = r.y + r.height / 2;
|
||||
const point = (x, y) => [{ x, y, id: 1, radiusX: 5, radiusY: 5, force: 1 }];
|
||||
try {
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: point(x, y) });
|
||||
for (let step = 1; step <= 5; step++)
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: point(x + dx * step / 5, y + dy * step / 5) });
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||||
await settle(page);
|
||||
} finally { await cdp.detach(); }
|
||||
}
|
||||
|
||||
function closeTo(actual, expected, label, tolerance = 1) {
|
||||
assert(Math.abs(actual - expected) <= tolerance, `${label}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
|
||||
function sameButtons(actual, expected, except = '') {
|
||||
for (const button of faceButtons.filter(button => button !== except)) for (const key of ['x', 'y', 'width', 'height'])
|
||||
closeTo(actual[button][key], expected[button][key], button + ' ' + key);
|
||||
}
|
||||
|
||||
async function resize(page, percent) {
|
||||
await page.locator('#lbSize').evaluate((slider, percent) => {
|
||||
slider.value = percent; slider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}, String(percent));
|
||||
await settle(page);
|
||||
}
|
||||
|
||||
async function checkOrientationOrdering(browser, fixture) {
|
||||
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
|
||||
await context.addInitScript(() => {
|
||||
const add = MediaQueryList.prototype.addEventListener;
|
||||
window.pendingOrientation = [];
|
||||
MediaQueryList.prototype.addEventListener = function(type, listener, options) {
|
||||
if (type === 'change' && this.media === '(orientation: portrait)') {
|
||||
return add.call(this, type, event => {
|
||||
if (window.deferOrientation) window.pendingOrientation.push(() => listener.call(this, event));
|
||||
else listener.call(this, event);
|
||||
}, options);
|
||||
}
|
||||
return add.call(this, type, listener, options);
|
||||
};
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.route('**/*', fixture);
|
||||
try {
|
||||
await page.goto('http://layout.test/');
|
||||
await page.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
await page.evaluate(() => { c4fLayoutTest.openPad(0); c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face'); });
|
||||
await page.selectOption('#lbFaceMode', 'separate');
|
||||
await drag(page, 'CROSS', -5000, 0);
|
||||
await drag(page, 'CROSS', 10, 0);
|
||||
const landscape = await page.evaluate(() => c4fLayoutTest.getLayout());
|
||||
await page.evaluate(() => { window.deferOrientation = true; });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.waitForFunction(() => pendingOrientation.length > 0);
|
||||
await settle(page);
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), landscape,
|
||||
'resize must not fit the outgoing landscape layout to portrait bounds');
|
||||
await page.evaluate(() => {
|
||||
window.deferOrientation = false;
|
||||
window.pendingOrientation.splice(0).forEach(run => run());
|
||||
});
|
||||
await settle(page);
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.editor.active), false);
|
||||
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('c4f.layout.landscape'))), landscape,
|
||||
'rotation saves the original landscape edit before loading portrait');
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.getLayout().face.separate), false);
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await settle(page);
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), landscape);
|
||||
console.log('PASS controller layout: resize before orientation preserves the outgoing edit');
|
||||
} finally { await context.close(); }
|
||||
}
|
||||
|
||||
async function checkComponentResets(browser, fixture) {
|
||||
const context = await browser.newContext({ viewport: { width: 1000, height: 600 }, locale: 'en-US' });
|
||||
const page = await context.newPage(), errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.route('**/*', fixture);
|
||||
const layout = () => page.evaluate(() => c4fLayoutTest.getLayout());
|
||||
const select = id => page.evaluate(id => c4fLayoutTest.editor.select(id), id);
|
||||
const open = () => page.evaluate(() => c4fLayoutTest.editor.open());
|
||||
async function move(id, dx, dy) {
|
||||
const r = await page.locator(`#padView [data-ctl="${id}"]`).boundingBox();
|
||||
await page.mouse.move(r.x + r.width / 2, r.y + r.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(r.x + r.width / 2 + dx, r.y + r.height / 2 + dy, { steps: 5 });
|
||||
await page.mouse.up();
|
||||
await settle(page);
|
||||
}
|
||||
try {
|
||||
await page.goto('http://layout.test/');
|
||||
await page.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
await page.evaluate(() => c4fLayoutTest.openPad(0));
|
||||
await settle(page);
|
||||
const defaults = await layout(), defaultButtons = await snapshot(page);
|
||||
await open();
|
||||
await select('r1'); await resize(page, 125); await page.click('#lbHide');
|
||||
// Buttons, sticks, D-pad and touchpad all use the same scoped Reset action.
|
||||
for (const id of ['l1', 'lstick', 'dpad', 'touchpad']) {
|
||||
await move(id, 30, 15); await select(id); await resize(page, 150); await page.click('#lbHide');
|
||||
const before = await layout();
|
||||
await page.click('#lbResetControl');
|
||||
const after = await layout();
|
||||
assert.deepEqual(after[id], defaults[id], id + ': Reset restores position, size and visibility');
|
||||
for (const other of Object.keys(before).filter(other => other !== id))
|
||||
assert.deepEqual(after[other], before[other], id + ': Reset preserves ' + other);
|
||||
assert.equal(await page.locator('#lbSize').inputValue(), '100');
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.editor.selected), id);
|
||||
}
|
||||
// A local reset is still an unsaved edit: Cancel restores saved custom data.
|
||||
await select('l1'); await resize(page, 150); await page.click('#lbHide');
|
||||
await page.click('#lbDone');
|
||||
const saved = await layout();
|
||||
await open(); await select('l1'); await page.click('#lbResetControl'); await page.click('#lbCancel');
|
||||
assert.deepEqual(await layout(), saved, 'Cancel undoes a component reset');
|
||||
await open(); await select('l1'); await page.click('#lbResetControl'); await page.click('#lbDone');
|
||||
const resetSaved = await layout();
|
||||
assert.deepEqual(resetSaved.l1, defaults.l1);
|
||||
assert.deepEqual(resetSaved.r1, saved.r1);
|
||||
await page.reload(); await page.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
await page.evaluate(() => c4fLayoutTest.openPad(0)); await settle(page);
|
||||
assert.deepEqual(await layout(), resetSaved, 'Done persists only the component reset across reload');
|
||||
|
||||
// Group scaling and rebasing must not make an individual reset move siblings.
|
||||
await open(); await select('face'); await resize(page, 150);
|
||||
await page.selectOption('#lbFaceMode', 'separate');
|
||||
await drag(page, 'CROSS', -100, 15);
|
||||
await select('face:CROSS'); await resize(page, 125); await page.click('#lbHide');
|
||||
await select('face:TRIANGLE'); await page.click('#lbHide');
|
||||
const custom = await layout(), customButtons = await snapshot(page);
|
||||
await select('face:CROSS'); await page.click('#lbResetControl');
|
||||
const reset = await layout(), resetButtons = await snapshot(page);
|
||||
sameButtons(resetButtons, customButtons, 'CROSS');
|
||||
for (const key of ['x', 'y', 'width', 'height'])
|
||||
closeTo(resetButtons.CROSS[key], defaultButtons.CROSS[key], 'Cross reset restores standard physical ' + key);
|
||||
assert.equal(reset.face.buttons.CROSS.hide, false);
|
||||
assert.equal(reset.face.buttons.TRIANGLE.hide, true);
|
||||
assert.equal(reset.face.separate, true, 'scoped reset preserves separate editing');
|
||||
for (const id of Object.keys(reset).filter(id => id !== 'face')) assert.deepEqual(reset[id], custom[id]);
|
||||
await page.click('#lbDone');
|
||||
assert.equal(await page.locator(faceSelector('TRIANGLE')).isHidden(), true);
|
||||
assert.equal(await page.locator(faceSelector('CROSS')).isVisible(), true);
|
||||
const faceSaved = await layout();
|
||||
await page.reload(); await page.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
await page.evaluate(() => c4fLayoutTest.openPad(0)); await settle(page);
|
||||
assert.deepEqual(await layout(), faceSaved, 'compensated individual size and position survive reload');
|
||||
|
||||
// Resetting a group resets its members while preserving its editing mode.
|
||||
await open(); await select('face'); await page.click('#lbResetControl');
|
||||
assert.deepEqual((await layout()).face, { ...defaults.face, separate: true });
|
||||
for (const mode of ['together', 'separate']) {
|
||||
await page.selectOption('#lbFaceMode', mode); await select('face'); await resize(page, 125);
|
||||
await page.click('#lbResetControl');
|
||||
assert.deepEqual((await layout()).face, { ...defaults.face, separate: mode === 'separate' });
|
||||
}
|
||||
// One child can be restored from a hidden group without exposing its siblings.
|
||||
await page.click('#lbHide'); await select('face:CROSS'); await page.click('#lbResetControl');
|
||||
await page.click('#lbDone');
|
||||
assert.equal(await page.locator(faceSelector('CROSS')).isVisible(), true);
|
||||
for (const button of faceButtons.filter(button => button !== 'CROSS'))
|
||||
assert.equal(await page.locator(faceSelector(button)).isHidden(), true, button + ': hidden sibling stays hidden');
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.screenSource.buttons), 0, 'Reset never presses controller inputs');
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS component Reset: default position/size/visibility, unchanged siblings, scaled face groups, hidden groups, mode preservation, Cancel and saved reload');
|
||||
} finally { await context.close(); }
|
||||
}
|
||||
|
||||
module.exports = async function checkControllerLayout(browser) {
|
||||
// The fixture deliberately does not connect or create a virtual controller.
|
||||
const hook = '\nwindow.c4fLayoutTest = { editor, openPad, mapper, getLayout: () => JSON.parse(JSON.stringify(layout)), screenSource, mergedState };\n';
|
||||
const original = fs.readFileSync('/src/client/index.html', 'utf8');
|
||||
const html = original.replace(' connect(address);', ' // No signaling in this offline fixture.')
|
||||
.replace('\n })();', hook + ' })();');
|
||||
assert.notEqual(html, original);
|
||||
const fixture = route => {
|
||||
const url = route.request().url();
|
||||
if (/^http:\/\/layout\.test\/(?:\?.*)?$/.test(url)) return route.fulfill({ contentType: 'text/html', body: html });
|
||||
const font = url === 'http://layout.test/fonts/arabic-light.otf' ? 'SSTArabic-Light.otf'
|
||||
: url === 'http://layout.test/fonts/arabic-regular.otf' ? 'SSTArabic-Roman.otf' : '';
|
||||
// Optional private fixtures validate the real console font without copying
|
||||
// Sony data into the repository or requiring console/network access.
|
||||
if (font && process.env.C4F_TEST_PS4_FONTS) return route.fulfill({ contentType: 'font/otf',
|
||||
body: fs.readFileSync(process.env.C4F_TEST_PS4_FONTS + '/' + font) });
|
||||
return route.abort();
|
||||
};
|
||||
const context = await browser.newContext({ viewport: { width: 1000, height: 600 }, locale: 'en-US' });
|
||||
const page = await context.newPage(), errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.route('**/*', fixture);
|
||||
try {
|
||||
await page.goto('http://layout.test/');
|
||||
await page.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
await page.evaluate(() => c4fLayoutTest.openPad(0));
|
||||
await settle(page);
|
||||
await page.evaluate(() => { c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face'); });
|
||||
assert.equal(await page.locator('#lbFaceMode').inputValue(), 'together', 'existing layouts keep grouped editing by default');
|
||||
let before = await snapshot(page);
|
||||
await drag(page, 'TRIANGLE', -80, -20);
|
||||
let after = await snapshot(page);
|
||||
for (const button of faceButtons) {
|
||||
closeTo(after[button].x - before[button].x, -80, button + ' grouped horizontal drag', 3);
|
||||
closeTo(after[button].y - before[button].y, -20, button + ' grouped vertical drag', 3);
|
||||
}
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.screenSource.buttons), 0, 'editing does not press game buttons');
|
||||
|
||||
before = after;
|
||||
await page.selectOption('#lbFaceMode', 'separate');
|
||||
await settle(page);
|
||||
sameButtons(await snapshot(page), before);
|
||||
await drag(page, 'CROSS', 20, 25);
|
||||
after = await snapshot(page);
|
||||
sameButtons(after, before, 'CROSS');
|
||||
closeTo(after.CROSS.x - before.CROSS.x, 20, 'Cross independent horizontal drag', 3);
|
||||
closeTo(after.CROSS.y - before.CROSS.y, 25, 'Cross independent vertical drag', 3);
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.editor.selected), 'face:CROSS');
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.screenSource.buttons), 0, 'independent editing cannot generate input');
|
||||
before = after;
|
||||
await resize(page, 150);
|
||||
after = await snapshot(page);
|
||||
sameButtons(after, before, 'CROSS');
|
||||
closeTo(after.CROSS.width / before.CROSS.width, 1.5, 'Cross independent size', .02);
|
||||
await page.click('#lbDone');
|
||||
const saved = await page.evaluate(() => c4fLayoutTest.getLayout());
|
||||
assert.equal(saved.face.separate, true);
|
||||
assert.equal(saved.face.buttons.CROSS.size, 150);
|
||||
const savedBounds = await snapshot(page);
|
||||
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('c4f.layout.landscape'))), saved);
|
||||
await page.reload();
|
||||
await page.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
await page.evaluate(() => c4fLayoutTest.openPad(0));
|
||||
await settle(page);
|
||||
|
||||
if (process.env.C4F_LAYOUT_PREVIEW_DIR) {
|
||||
fs.mkdirSync(process.env.C4F_LAYOUT_PREVIEW_DIR, { recursive: true });
|
||||
await page.evaluate(() => { c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face:CROSS'); document.querySelector('#toast').style.visibility = 'hidden'; });
|
||||
await page.screenshot({ path: process.env.C4F_LAYOUT_PREVIEW_DIR + '/en-layout-landscape.png' });
|
||||
await page.click('#lbCancel');
|
||||
}
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), saved, 'individual positions and sizes survive a reload');
|
||||
sameButtons(await snapshot(page), savedBounds);
|
||||
|
||||
// The mapping picture is a standard DS4 reference, regardless of the
|
||||
// persisted touch layout that was cloned when this document initialized.
|
||||
await page.evaluate(() => c4fLayoutTest.mapper.open('keyboard'));
|
||||
await settle(page);
|
||||
assert.equal(await page.locator('#mapView').isVisible(), true);
|
||||
const picture = await snapshot(page, '#mapView');
|
||||
for (const button of faceButtons) {
|
||||
closeTo(picture[button].width, picture.TRIANGLE.width, button + ': mapping picture retains standard size');
|
||||
closeTo(picture[button].height, picture.TRIANGLE.height, button + ': mapping picture retains standard height');
|
||||
}
|
||||
closeTo(picture.TRIANGLE.x, picture.CROSS.x, 'mapping picture vertical column');
|
||||
closeTo(picture.SQUARE.y, picture.CIRCLE.y, 'mapping picture horizontal row');
|
||||
closeTo(picture.TRIANGLE.x, (picture.SQUARE.x + picture.CIRCLE.x) / 2, 'mapping picture horizontal centre');
|
||||
closeTo(picture.SQUARE.y, (picture.TRIANGLE.y + picture.CROSS.y) / 2, 'mapping picture vertical centre');
|
||||
assert(picture.TRIANGLE.y < picture.CROSS.y && picture.SQUARE.x < picture.CIRCLE.x, 'mapping picture retains its standard diamond');
|
||||
await page.evaluate(() => c4fLayoutTest.mapper.close());
|
||||
await settle(page);
|
||||
assert.equal(await page.locator('#padView').isVisible(), true);
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), saved, 'leaving mapping retains the saved custom touch arrangement');
|
||||
sameButtons(await snapshot(page), savedBounds);
|
||||
|
||||
// The physical buttons retain their original input bits after repositioning.
|
||||
for (const [button, bit] of [['TRIANGLE', 0x1000], ['CIRCLE', 0x2000], ['CROSS', 0x4000], ['SQUARE', 0x8000]]) {
|
||||
const r = await page.locator(faceSelector(button)).boundingBox();
|
||||
await page.mouse.move(r.x + r.width / 2, r.y + r.height / 2);
|
||||
await page.mouse.down();
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.screenSource.buttons), bit, button + ' keeps its input mapping');
|
||||
await page.mouse.up();
|
||||
await page.waitForFunction(() => c4fLayoutTest.screenSource.buttons === 0);
|
||||
}
|
||||
await settle(page);
|
||||
|
||||
// Regrouping must keep the custom arrangement and transform it as one unit.
|
||||
await page.evaluate(() => { c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face'); });
|
||||
before = await snapshot(page);
|
||||
await page.selectOption('#lbFaceMode', 'together');
|
||||
await settle(page);
|
||||
sameButtons(await snapshot(page), before);
|
||||
await drag(page, 'TRIANGLE', -35, -15);
|
||||
after = await snapshot(page);
|
||||
for (const button of faceButtons) {
|
||||
closeTo(after[button].x - before[button].x, -35, button + ' regrouped horizontal drag', 3);
|
||||
closeTo(after[button].y - before[button].y, -15, button + ' regrouped vertical drag', 3);
|
||||
}
|
||||
before = after;
|
||||
await resize(page, 125);
|
||||
after = await snapshot(page);
|
||||
for (const button of faceButtons) closeTo(after[button].width / before[button].width, 1.25, button + ' group resize', .02);
|
||||
await page.click('#lbCancel');
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), saved, 'Cancel restores mode, child positions and sizes');
|
||||
sameButtons(await snapshot(page), savedBounds);
|
||||
|
||||
// Individual hiding affects one hit target and can be undone from the editor.
|
||||
await page.evaluate(() => { c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face:CROSS'); });
|
||||
await page.click('#lbHide');
|
||||
await page.click('#lbDone');
|
||||
assert.equal(await page.locator(faceSelector('CROSS')).isHidden(), true);
|
||||
for (const button of faceButtons.filter(button => button !== 'CROSS')) assert.equal(await page.locator(faceSelector(button)).isVisible(), true);
|
||||
await page.evaluate(() => { c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face:CROSS'); });
|
||||
assert.equal(await page.locator(faceSelector('CROSS')).isVisible(), true, 'hidden buttons remain editable');
|
||||
await page.click('#lbHide');
|
||||
await page.click('#lbDone');
|
||||
|
||||
// Leaving the orientation saves the active edit in its original orientation.
|
||||
await page.evaluate(() => c4fLayoutTest.editor.open());
|
||||
await drag(page, 'CROSS', 20, -10);
|
||||
const landscape = await page.evaluate(() => c4fLayoutTest.getLayout());
|
||||
await page.setViewportSize({ width: 600, height: 1000 });
|
||||
await page.waitForFunction(() => !c4fLayoutTest.editor.active && matchMedia('(orientation: portrait)').matches);
|
||||
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('c4f.layout.landscape'))), landscape);
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.getLayout().face.separate), false, 'portrait starts with its own grouped default');
|
||||
if (await page.locator('#portraitOk').isVisible()) await page.click('#portraitOk');
|
||||
await page.evaluate(() => { c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face'); });
|
||||
await page.selectOption('#lbFaceMode', 'separate');
|
||||
await drag(page, 'SQUARE', -20, 25);
|
||||
await page.click('#lbDone');
|
||||
const portrait = await page.evaluate(() => c4fLayoutTest.getLayout());
|
||||
await page.setViewportSize({ width: 1000, height: 600 });
|
||||
await page.waitForFunction(() => !matchMedia('(orientation: portrait)').matches);
|
||||
await settle(page);
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), landscape, 'landscape retains its separate positions');
|
||||
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('c4f.layout.portrait'))), portrait);
|
||||
|
||||
// Reset remains a two-tap operation; Cancel also restores a reset layout.
|
||||
await page.evaluate(() => c4fLayoutTest.editor.open());
|
||||
await page.click('#lbReset');
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), landscape, 'first reset tap is non-destructive');
|
||||
await page.click('#lbReset');
|
||||
assert.equal(await page.evaluate(() => c4fLayoutTest.getLayout().face.separate), false);
|
||||
await page.click('#lbCancel');
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), landscape);
|
||||
await page.evaluate(() => c4fLayoutTest.editor.open());
|
||||
await page.click('#lbReset'); await page.click('#lbReset'); await page.click('#lbDone');
|
||||
assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem('c4f.layout.landscape')).face.separate), false, 'Done persists the reset');
|
||||
|
||||
// Old layout records have no child metadata: preserve their grouped position.
|
||||
await page.evaluate(() => localStorage.setItem('c4f.layout.landscape', JSON.stringify({ face: { x: 73, y: 51, w: 32, h: 32, hide: false } })));
|
||||
await page.reload(); await page.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
await page.evaluate(() => { c4fLayoutTest.openPad(0); c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face'); });
|
||||
const legacy = await page.evaluate(() => c4fLayoutTest.getLayout().face);
|
||||
assert.deepEqual([legacy.x, legacy.y, legacy.w, legacy.h, legacy.hide, legacy.separate], [73, 51, 32, 32, false, false]);
|
||||
before = await snapshot(page);
|
||||
await page.selectOption('#lbFaceMode', 'separate');
|
||||
await settle(page);
|
||||
sameButtons(await snapshot(page), before);
|
||||
|
||||
// Individually moved controls and their regrouped bounds stay on screen.
|
||||
await drag(page, 'CROSS', -2000, -2000);
|
||||
const edge = await snapshot(page);
|
||||
assert(edge.CROSS.x - edge.CROSS.width / 2 >= -.6 && edge.CROSS.y - edge.CROSS.height / 2 >= -.6, 'individual drag clamps the entire button');
|
||||
sameButtons(edge, before, 'CROSS');
|
||||
await page.selectOption('#lbFaceMode', 'together');
|
||||
await drag(page, 'CROSS', 30, 30);
|
||||
const outsideDrag = await snapshot(page);
|
||||
for (const button of faceButtons) {
|
||||
closeTo(outsideDrag[button].x - edge[button].x, 30, button + ': grouped drag starts on an out-of-parent child', 3);
|
||||
closeTo(outsideDrag[button].y - edge[button].y, 30, button + ': out-of-parent child moves the group vertically', 3);
|
||||
}
|
||||
await drag(page, 'TRIANGLE', 2000, 2000);
|
||||
for (const [button, r] of Object.entries(await snapshot(page)))
|
||||
assert(r.x - r.width / 2 >= -.6 && r.y - r.height / 2 >= -.6 && r.x + r.width / 2 <= 1000.6 && r.y + r.height / 2 <= 600.6, button + ': regrouped custom arrangement stays on screen');
|
||||
await resize(page, 200);
|
||||
for (const [button, r] of Object.entries(await snapshot(page)))
|
||||
assert(r.x - r.width / 2 >= -.6 && r.y - r.height / 2 >= -.6 && r.x + r.width / 2 <= 1000.6 && r.y + r.height / 2 <= 600.6, button + ': grouped resize clamps the full custom arrangement');
|
||||
await page.click('#lbCancel');
|
||||
const stable = await page.evaluate(() => c4fLayoutTest.getLayout());
|
||||
await page.setViewportSize({ width: 1200, height: 700 });
|
||||
await settle(page);
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), stable, 'same-orientation resize preserves the saved layout');
|
||||
await page.setViewportSize({ width: 900, height: 620 });
|
||||
await settle(page);
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), stable);
|
||||
for (const [button, r] of Object.entries(await snapshot(page)))
|
||||
assert(r.x - r.width / 2 >= -.6 && r.y - r.height / 2 >= -.6 && r.x + r.width / 2 <= 900.6 && r.y + r.height / 2 <= 620.6, button + ': same-orientation resize keeps the hit target in view');
|
||||
|
||||
// A desktop arrangement spanning most of the screen also fits a phone in
|
||||
// landscape, where --u and the viewport change by different proportions.
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await page.evaluate(() => localStorage.removeItem('c4f.layout.landscape'));
|
||||
await page.reload(); await page.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
await page.evaluate(() => { c4fLayoutTest.openPad(0); c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face'); });
|
||||
await page.selectOption('#lbFaceMode', 'separate');
|
||||
await drag(page, 'CROSS', -5000, 0);
|
||||
await page.click('#lbDone');
|
||||
await page.setViewportSize({ width: 844, height: 390 });
|
||||
await settle(page);
|
||||
for (const [button, r] of Object.entries(await snapshot(page))) {
|
||||
assert(r.x - r.width / 2 >= -.6 && r.y - r.height / 2 >= -.6 && r.x + r.width / 2 <= 844.6 && r.y + r.height / 2 <= 390.6, button + ': saved desktop edge position remains accessible on a landscape phone');
|
||||
assert.equal(await page.evaluate(({ button, x, y }) => document.elementFromPoint(x, y)?.closest('.face-btn')?.dataset.btn === button,
|
||||
{ button, x: r.x, y: r.y }), true, button + ': phone button centre receives pointer input');
|
||||
}
|
||||
|
||||
// A hidden ancestor produces zero child rectangles, even when the child
|
||||
// itself has no hidden attribute. Safe-area offsets must not turn those
|
||||
// zero rectangles into false overflow and repeatedly move the saved group.
|
||||
await page.evaluate(() => { c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face'); });
|
||||
await page.click('#lbHide'); await page.click('#lbDone');
|
||||
assert.equal(await page.locator('#padView [data-ctl="face"]').isHidden(), true);
|
||||
const hiddenLayout = await page.evaluate(() => c4fLayoutTest.getLayout());
|
||||
await page.locator('#padView .pad-area').evaluate(node => { node.style.inset = '20px'; });
|
||||
for (const [width, height] of [[1000, 500], [900, 450], [1200, 700], [844, 390], [1000, 500], [844, 390]]) {
|
||||
await page.setViewportSize({ width, height });
|
||||
await settle(page);
|
||||
assert.deepEqual(await page.evaluate(() => c4fLayoutTest.getLayout()), hiddenLayout,
|
||||
'hidden face group never drifts during viewport resize with 20px safe-area inset');
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS controller layout: grouped compatibility, separate drag/size/hide and input, regrouping, save/reload, Cancel/reset, orientation, landscape phone fitting and hidden safe-area stability');
|
||||
} finally { await context.close(); }
|
||||
|
||||
await checkOrientationOrdering(browser, fixture);
|
||||
await checkComponentResets(browser, fixture);
|
||||
|
||||
// Compact editor controls fit translated labels; controller geometry stays LTR.
|
||||
for (const locale of ['en-US', 'pt-BR', 'ar-SA']) for (const width of [320, 390, 1000]) {
|
||||
const narrow = await browser.newContext({ viewport: { width, height: width === 1000 ? 600 : 900 }, locale, hasTouch: width < 1000 });
|
||||
const p = await narrow.newPage();
|
||||
await p.route('**/*', fixture);
|
||||
try {
|
||||
await p.goto('http://layout.test/'); await p.waitForFunction(() => !!window.c4fLayoutTest);
|
||||
if (locale === 'ar-SA') {
|
||||
await p.evaluate(() => c4fI18n.loadConsoleFonts('http://layout.test/'));
|
||||
if (process.env.C4F_TEST_PS4_FONTS) assert.deepEqual(await p.evaluate(() =>
|
||||
[...document.fonts].filter(font => font.family.replaceAll(String.fromCharCode(34), '') === 'SST Arabic' && font.status === 'loaded').map(font => font.weight).sort()), ['300', '400'],
|
||||
'both actual console font weights load before Arabic panel measurements');
|
||||
await p.evaluate(() => document.fonts.ready);
|
||||
}
|
||||
await p.evaluate(() => c4fLayoutTest.openPad(0));
|
||||
if (await p.locator('#portraitOk').isVisible()) await p.click('#portraitOk');
|
||||
await p.evaluate(() => { c4fLayoutTest.editor.open(); c4fLayoutTest.editor.select('face'); });
|
||||
await p.selectOption('#lbFaceMode', 'separate');
|
||||
for (const id of [...Object.keys(await p.evaluate(() => c4fLayoutTest.getLayout())), ...faceButtons.map(button => 'face:' + button)]) {
|
||||
await p.evaluate(id => c4fLayoutTest.editor.select(id), id);
|
||||
const r = await p.locator('#layoutPanel').boundingBox();
|
||||
assert(r.x >= -1 && r.x + r.width <= width + 1, locale + ': ' + id + ' panel fits ' + width + 'px');
|
||||
assert.equal(await p.locator('#layoutPanel').evaluate(node => node.scrollWidth <= node.clientWidth + 1), true,
|
||||
locale + ': ' + id + ' panel has no internal overflow');
|
||||
assert.equal(await p.locator('#lbResetControl').isVisible(), true, locale + ': ' + id + ' has its own Reset');
|
||||
}
|
||||
await p.evaluate(() => c4fLayoutTest.editor.select('face:CROSS'));
|
||||
await settle(p);
|
||||
const panel = await p.locator('#layoutPanel').boundingBox();
|
||||
assert(panel.x >= -1 && panel.x + panel.width <= width + 1, locale + ': selection panel fits ' + width + 'px');
|
||||
assert.equal(await p.locator('#layoutPanel').evaluate(e => e.scrollWidth <= e.clientWidth + 1), true, locale + ': no internal panel overflow');
|
||||
assert.equal(await p.locator('#lbResetControl').isVisible(), true, locale + ': component Reset is visible beside Hide');
|
||||
assert.equal(await p.locator('#lbFaceMode').evaluate(select => {
|
||||
const style = getComputedStyle(select), canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d'); context.font = style.font;
|
||||
const text = Math.max(...[...select.options].map(option => context.measureText(option.textContent).width));
|
||||
const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
||||
return text + padding + 20 <= select.clientWidth + 1;
|
||||
}), true, locale + ': full mode labels fit beside the native selector arrow');
|
||||
const positions = await snapshot(p);
|
||||
assert(positions.TRIANGLE.y < positions.CROSS.y && positions.SQUARE.x < positions.CIRCLE.x, locale + ': face button positions never mirror');
|
||||
await drag(p, 'CROSS', -25, 15);
|
||||
const moved = await snapshot(p);
|
||||
sameButtons(moved, positions, 'CROSS');
|
||||
closeTo(moved.CROSS.x - positions.CROSS.x, -25, locale + ': physical horizontal drag', 2);
|
||||
if (width === 320) {
|
||||
await touchDrag(p, 'CROSS', 15, 10);
|
||||
const touched = await snapshot(p);
|
||||
sameButtons(touched, moved, 'CROSS');
|
||||
closeTo(touched.CROSS.x - moved.CROSS.x, 15, locale + ': real touch independently moves Cross horizontally', 2);
|
||||
closeTo(touched.CROSS.y - moved.CROSS.y, 10, locale + ': real touch independently moves Cross vertically', 2);
|
||||
assert.equal(await p.evaluate(() => c4fLayoutTest.screenSource.buttons), 0, 'touch editing does not press Cross');
|
||||
}
|
||||
if (process.env.C4F_LAYOUT_PREVIEW_DIR && locale !== 'pt-BR') {
|
||||
await p.evaluate(() => document.querySelector('#toast').style.visibility = 'hidden');
|
||||
await p.screenshot({ path: process.env.C4F_LAYOUT_PREVIEW_DIR + '/' + (locale === 'ar-SA' ? 'ar' : 'en') + '-layout-' + width + '.png' });
|
||||
}
|
||||
await p.click('#lbDone');
|
||||
assert.equal(await p.evaluate(() => c4fLayoutTest.getLayout().face.separate), true);
|
||||
} finally { await narrow.close(); }
|
||||
}
|
||||
console.log('PASS controller layout: English/Portuguese/Arabic controls fit 320/390px and landscape; real touch dragging preserves physical directions and input neutrality');
|
||||
};
|
||||
|
||||
if (require.main === module) (async () => {
|
||||
const browser = await require('playwright').chromium.launch({ headless: true });
|
||||
try { await module.exports(browser); } finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
+2
-1
@@ -10,7 +10,8 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SUITES = ['test_logging', 'test_frontend', 'test_web', 'test_recovery', 'test_launcher', 'test_rtc']
|
||||
SUITES = ['test_logging', 'test_frontend', 'test_web', 'test_recovery', 'test_launcher',
|
||||
'test_rtc', 'test_text_dependencies', 'test_launcher_text']
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""The page's own claim and Gamepad functions, run under Node."""
|
||||
"""The page's own claim, Gamepad and language functions, run under Node."""
|
||||
import subprocess
|
||||
|
||||
|
||||
def main():
|
||||
subprocess.run(['node', 'tests/frontend_test.cjs'], check=True, timeout=30)
|
||||
subprocess.run(['node', 'tests/mapping_test.cjs'], check=True, timeout=30)
|
||||
subprocess.run(['node', 'tests/i18n_test.cjs'], check=True, timeout=30)
|
||||
subprocess.run(['python3', '-B', 'tests/release_notes_test.py'], check=True, timeout=10)
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,11 @@ def main():
|
||||
*(['-DC4F_DIAG'] if diagnostic else []), '-Ilauncher', 'launcher/tester.c',
|
||||
'launcher/navigation.c', 'tests/launcher_tester_test.c', '-lm', '-o', binary], check=True)
|
||||
subprocess.run([binary], check=True)
|
||||
# The app's Portuguese table and when it is used.
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror', '-Ilauncher',
|
||||
'tests/launcher_language_test.c', 'launcher/message.c', '-o', 'build/launcher-language-host-test'], check=True)
|
||||
subprocess.run(['build/launcher-language-host-test'], check=True)
|
||||
subprocess.run([sys.executable, '-B', 'tests/test_launcher_language_render.py'], check=True)
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
|
||||
'-ffunction-sections', '-fdata-sections', '-Ilauncher', '-Itests/include',
|
||||
'tests/diag_test.c', '-Wl,--gc-sections,--wrap=write,--wrap=fsync', '-pthread',
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Language changes must invalidate every text-bearing tester cache."""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import test_tester_render
|
||||
|
||||
|
||||
def main():
|
||||
binary = test_tester_render.build(
|
||||
output=Path('build/launcher/language-render-test').resolve(),
|
||||
fixture='tests/launcher_language_render_test.c', extra_sources=['launcher/language.c'])
|
||||
subprocess.run([str(binary), 'names'], check=True, timeout=30)
|
||||
for language in ('en', 'pt', 'ar'):
|
||||
for variant in range(6):
|
||||
fresh = subprocess.check_output([str(binary), language, 'fresh', str(variant)], timeout=60)
|
||||
warm = subprocess.check_output([str(binary), language, 'warm', str(variant)], timeout=60)
|
||||
assert fresh == warm, (language, variant, fresh, warm)
|
||||
print('PASS all RGB focus/precision variants match fresh rendering after both language-switch directions', flush=True)
|
||||
for language in ('pt', 'ar'):
|
||||
expected = []
|
||||
for variant in range(4):
|
||||
fresh = subprocess.check_output([str(binary), 'metadata', language, 'fresh', str(variant)], timeout=60)
|
||||
warm = subprocess.check_output([str(binary), 'metadata', language, 'warm', str(variant)], timeout=60)
|
||||
assert fresh == warm, ('metadata', language, variant, fresh, warm)
|
||||
expected.append(fresh)
|
||||
assert expected[0] != expected[1], ('generated name was not visibly distinct', language)
|
||||
assert expected[2] != expected[3], ('message argument provenance was not visibly distinct', language)
|
||||
print('PASS generated names and message provenance invalidate tools cache in both directions', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Build and test the shipped Arabic renderer, entirely inside Docker."""
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--font-dir', type=Path, help='Optional privately supplied PS4 fonts; never bundled')
|
||||
args = parser.parse_args()
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import package_launcher as package
|
||||
package.embed_fonts()
|
||||
includes, libraries = package.text_host_flags()
|
||||
output = ROOT / 'build/launcher/text-render-host-test'
|
||||
flags = ['clang-18', '-std=gnu11', '-O2', '-D_DEFAULT_SOURCE', '-Ilauncher', '-Ivendor/stb', *includes]
|
||||
stb = output.with_suffix('.stb.o')
|
||||
subprocess.run([*flags, '-w', '-c', 'launcher/stb_truetype.c', '-o', str(stb)], cwd=ROOT, check=True)
|
||||
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', 'tests/launcher_text_test.c',
|
||||
'launcher/draw.c', str(stb), str(package.FONTS), *libraries, '-lm', '-o', str(output)], cwd=ROOT, check=True)
|
||||
subprocess.run([str(output)], cwd=ROOT, check=True, timeout=30)
|
||||
if args.font_dir:
|
||||
subprocess.run([str(output), str(args.font_dir / 'SSTArabic-Light.otf'),
|
||||
str(args.font_dir / 'SSTArabic-Roman.otf')], cwd=ROOT, check=True, timeout=30)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -12,21 +12,24 @@ import sys
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def build(renderer=None, output=None):
|
||||
def build(renderer=None, output=None, fixture=None, extra_sources=()):
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import package_launcher as package
|
||||
package.embed_fonts()
|
||||
text_includes, text_libraries = package.text_host_flags()
|
||||
output = Path(output or ROOT / 'build/launcher/tester-render-host-test')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
version = (ROOT / 'VERSION').read_text().strip()
|
||||
flags = ['clang-18', '-std=gnu11', '-O2', '-D_DEFAULT_SOURCE', '-Ilauncher',
|
||||
'-Ivendor/stb', '-Ivendor/qrcodegen', f'-DC4F_LAUNCHER_VERSION="{version}"']
|
||||
'-Ivendor/stb', '-Ivendor/qrcodegen', f'-DC4F_LAUNCHER_VERSION="{version}"', *text_includes]
|
||||
stb = output.with_suffix('.stb.o')
|
||||
subprocess.run([*flags, '-w', '-c', 'launcher/stb_truetype.c', '-o', str(stb)], cwd=ROOT, check=True)
|
||||
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', 'tests/tester_render_test.c',
|
||||
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', str(fixture or 'tests/tester_render_test.c'),
|
||||
'launcher/screen.c', 'launcher/navigation.c', 'launcher/tester.c',
|
||||
str(renderer or 'launcher/tester_draw.c'), 'launcher/draw.c', 'launcher/logo.c',
|
||||
'vendor/qrcodegen/qrcodegen.c', str(package.FONTS), str(stb), '-lm', '-o', str(output)],
|
||||
str(renderer or 'launcher/tester_draw.c'), 'launcher/draw.c', 'launcher/text.c', 'launcher/logo.c',
|
||||
'launcher/message.c', 'vendor/qrcodegen/qrcodegen.c', str(package.FONTS), str(stb), *map(str, extra_sources),
|
||||
*([] if 'launcher/language.c' in extra_sources else ['launcher/language.c', 'tests/language_system_stub.c']),
|
||||
*text_libraries, '-lm', '-o', str(output)],
|
||||
cwd=ROOT, check=True)
|
||||
return output
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Verify real Arabic shaping and a PS4 link with no additional runtime."""
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
spec = importlib.util.spec_from_file_location("build_text", ROOT / "tools/build_text.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
host = module.build(ROOT / "build/text-host", host=True)
|
||||
includes = ["-Ivendor/harfbuzz/src", "-Ivendor/sheenbidi/Headers"]
|
||||
archives = [str(host / "libharfbuzz.a"), str(host / "libsheenbidi.a")]
|
||||
executable = host / "text-dependencies-test"
|
||||
subprocess.run(["clang-18", "-std=c11", "-Wall", "-Wextra", "-Werror", *includes,
|
||||
"tests/text_dependencies_test.c", *archives, "-lm", "-o", str(executable)],
|
||||
cwd=ROOT, check=True)
|
||||
fonts = ROOT / "vendor/noto-sans-arabic"
|
||||
subprocess.run([str(executable), str(fonts / "NotoSansArabic-Light.ttf"),
|
||||
str(fonts / "NotoSansArabic-Regular.ttf")], check=True, timeout=15)
|
||||
sdk = Path(os.environ["OO_PS4_TOOLCHAIN"])
|
||||
native = module.build(ROOT / "build/text")
|
||||
obj = native / "gate.o"
|
||||
elf = native / "gate.elf"
|
||||
subprocess.run(["clang-18", "--target=x86_64-pc-freebsd12-elf", "-fPIC", "-isysroot",
|
||||
str(sdk), "-isystem", str(sdk / "include"), *includes, "-c",
|
||||
"tests/text_dependencies_test.c", "-o", str(obj)], cwd=ROOT, check=True)
|
||||
subprocess.run(["ld.lld-18", "-m", "elf_x86_64", "-pie", "--script", str(sdk / "link.x"),
|
||||
"--eh-frame-hdr", "-o", str(elf), str(obj), str(native / "libharfbuzz.a"),
|
||||
str(native / "libsheenbidi.a"), "-L" + str(sdk / "lib"), "-lc", "-lkernel",
|
||||
str(sdk / "lib/crt1.o")], check=True)
|
||||
dynamic = subprocess.check_output(["llvm-readelf-18", "-d", str(elf)], text=True)
|
||||
needed = [line for line in dynamic.splitlines() if "(NEEDED)" in line]
|
||||
assert needed and all("[libkernel.so]" in line for line in needed), needed
|
||||
print("Arabic text dependencies: OpenOrbis static link, no C++ runtime PASS", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -19,11 +19,22 @@ BINARY = ROOT / 'build/web-host-test'
|
||||
|
||||
|
||||
def build():
|
||||
# Redistributable fixtures exercise runtime system-font loading. Sony font
|
||||
# files are neither test inputs nor release assets.
|
||||
font_root = ROOT / 'build/system-font-test'
|
||||
(font_root / 'primary').mkdir(parents=True, exist_ok=True)
|
||||
(font_root / 'fallback').mkdir(exist_ok=True)
|
||||
(font_root / 'primary/SSTArabic-Light.otf').write_bytes(b'bad font')
|
||||
for weight, source, directory in [('Light', 'Light', 'fallback'), ('Roman', 'Regular', 'primary')]:
|
||||
(font_root / directory / f'SSTArabic-{weight}.otf').write_bytes(
|
||||
(ROOT / f'vendor/noto-sans-arabic/NotoSansArabic-{source}.ttf').read_bytes())
|
||||
subprocess.run(['python3', 'tools/embed_client.py', 'client/index.html', 'build/client.c'], check=True)
|
||||
subprocess.run(['python3', 'tools/embed_file.py', 'build/assets.c',
|
||||
'c4fManifest=client/manifest.webmanifest', 'c4fIcon=client/icon-192.png'], check=True)
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-Wall', '-Wextra', '-Werror', '-g',
|
||||
'-Iinclude', '-Ivendor/jsmn', '-Ivendor/qrcodegen', '-DC4F_TEST_WEBSOCKET', '-DC4F_STALE_MS=400', '-DC4F_RELEASE_MS=1800', '-DC4F_LATENCY_MS=700',
|
||||
'-DC4F_SYSTEM_FONT_PRIMARY="build/system-font-test/primary/"',
|
||||
'-DC4F_SYSTEM_FONT_FALLBACK="build/system-font-test/fallback/"',
|
||||
*mbedtls_host.FLAGS,
|
||||
'src/net.c', 'src/web.c', 'src/rtc.c', 'src/stun.c', 'src/sctp.c', 'src/klog_line.c',
|
||||
'vendor/qrcodegen/qrcodegen.c', 'tests/web_stub.c', 'build/client.c',
|
||||
@@ -295,6 +306,21 @@ def main():
|
||||
assert b'403' in http(b'GET /icon-192.png HTTP/1.1\r\nHost: evil.example:4264\r\n\r\n')
|
||||
print('PASS manifest and icon served for a home-screen shortcut', flush=True)
|
||||
|
||||
assert b"font-src 'self' data:" in http(b'GET / HTTP/1.1\r\nHost: 127.0.0.1:4264\r\n\r\n')
|
||||
for route, weight in [('light', 'Light'), ('regular', 'Regular')]:
|
||||
path = f'/fonts/arabic-{route}.otf'
|
||||
headers, body = http(f'GET {path} HTTP/1.1\r\nHost: 127.0.0.1:4264\r\nOrigin: null\r\n\r\n'.encode()).split(b'\r\n\r\n', 1)
|
||||
assert b'200 OK' in headers and b'Content-Type: font/otf' in headers
|
||||
assert b'Access-Control-Allow-Origin: null' in headers
|
||||
assert body == (ROOT / f'vendor/noto-sans-arabic/NotoSansArabic-{weight}.ttf').read_bytes()
|
||||
assert b'403' in http(f'GET {path} HTTP/1.1\r\nHost: evil.example\r\n\r\n'.encode())
|
||||
assert b'403' in http(f'GET {path} HTTP/1.1\r\nHost: 127.0.0.1:4264\r\nOrigin: https://evil.example\r\n\r\n'.encode())
|
||||
headers = http(f'OPTIONS {path} HTTP/1.1\r\nHost: 127.0.0.1:4264\r\nOrigin: null\r\n\r\n'.encode())
|
||||
assert b'204 No Content' in headers and b'Access-Control-Allow-Methods: GET' in headers
|
||||
for path in ['/fonts/../SSTArabic-Bold.otf', '/fonts/arabic-bold.otf', '/fonts/arabic-light.otf?file=/etc/passwd']:
|
||||
assert b'404' in http(f'GET {path} HTTP/1.1\r\nHost: 127.0.0.1:4264\r\n\r\n'.encode())
|
||||
print('PASS bounded system font assets, fallback directory, saved-page CORS and strict paths', flush=True)
|
||||
|
||||
c = server.c
|
||||
assert c.request('info')['result']['pads'] == 4
|
||||
c.frame(b'ping', op=9)
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "screen.h"
|
||||
#include "language.h"
|
||||
#include "draw.h"
|
||||
|
||||
static const char *c4fCases[] = {
|
||||
"idle", "sticks", "buttons", "motion", "touch", "red", "blue", "rumble", "note",
|
||||
@@ -187,6 +189,10 @@ int main(int argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
if (c4fScreenInit()) return 2;
|
||||
const char *language = getenv("C4F_TEST_LANGUAGE");
|
||||
if (getenv("C4F_TEST_SYSTEM_FONTS") && c4fTextLoadSystemFonts()) return 3;
|
||||
if (language) c4fLanguageUse(!strcmp(language, "ar") ? C4F_LANGUAGE_ARABIC :
|
||||
!strcmp(language, "pt") ? C4F_LANGUAGE_PORTUGUESE : C4F_LANGUAGE_ENGLISH);
|
||||
uint32_t *pixels = calloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT, sizeof(*pixels));
|
||||
if (!pixels) return 2;
|
||||
C4fLauncherScreen state;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* Exercise the shipped shaping and bidi libraries with the shipped static fonts. */
|
||||
#include <hb.h>
|
||||
#include <hb-ot.h>
|
||||
#include <SheenBidi/SheenBidi.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CHECK(value) do { if (!(value)) { \
|
||||
fprintf(stderr, "text dependency check failed: %s:%d: %s\n", __FILE__, __LINE__, #value); \
|
||||
exit(1); } } while (0)
|
||||
|
||||
static void c4fTestFont(const char *path)
|
||||
{
|
||||
FILE *file = fopen(path, "rb");
|
||||
CHECK(file);
|
||||
CHECK(fseek(file, 0, SEEK_END) == 0);
|
||||
long size = ftell(file);
|
||||
CHECK(size > 0);
|
||||
rewind(file);
|
||||
char *bytes = malloc((size_t)size);
|
||||
CHECK(bytes && fread(bytes, 1, (size_t)size, file) == (size_t)size);
|
||||
fclose(file);
|
||||
hb_blob_t *blob = hb_blob_create(bytes, (unsigned)size, HB_MEMORY_MODE_READONLY, bytes, free);
|
||||
hb_face_t *face = hb_face_create(blob, 0);
|
||||
hb_font_t *font = hb_font_create(face);
|
||||
hb_ot_font_set_funcs(font);
|
||||
hb_font_set_scale(font, 32 * 64, 32 * 64);
|
||||
|
||||
hb_codepoint_t nominal = 0;
|
||||
CHECK(hb_font_get_nominal_glyph(font, 0x0628, &nominal)); /* beh */
|
||||
hb_buffer_t *buffer = hb_buffer_create();
|
||||
hb_buffer_add_utf8(buffer, "ببب", -1, 0, -1);
|
||||
hb_buffer_set_direction(buffer, HB_DIRECTION_RTL);
|
||||
hb_buffer_set_script(buffer, HB_SCRIPT_ARABIC);
|
||||
hb_buffer_set_language(buffer, hb_language_from_string("ar", -1));
|
||||
hb_shape(font, buffer, NULL, 0);
|
||||
unsigned count = 0;
|
||||
hb_glyph_info_t *infos = hb_buffer_get_glyph_infos(buffer, &count);
|
||||
/* Noto Arabic decomposes each beh into a dot and a contextual body. */
|
||||
CHECK(count == 6);
|
||||
CHECK(infos[1].cluster == 4 && infos[3].cluster == 2 && infos[5].cluster == 0);
|
||||
CHECK(infos[1].codepoint && infos[3].codepoint && infos[5].codepoint);
|
||||
CHECK(infos[1].codepoint != nominal && infos[3].codepoint != nominal &&
|
||||
infos[5].codepoint != nominal);
|
||||
CHECK(infos[1].codepoint != infos[3].codepoint && infos[3].codepoint != infos[5].codepoint);
|
||||
|
||||
hb_buffer_clear_contents(buffer);
|
||||
hb_buffer_add_utf8(buffer, "مَرْحَبًا", -1, 0, -1);
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(font, buffer, NULL, 0);
|
||||
infos = hb_buffer_get_glyph_infos(buffer, &count);
|
||||
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(buffer, NULL);
|
||||
unsigned marks = 0;
|
||||
for (unsigned i = 0; i < count; ++i) {
|
||||
CHECK(infos[i].codepoint != 0);
|
||||
if (!positions[i].x_advance && (positions[i].x_offset || positions[i].y_offset))
|
||||
++marks;
|
||||
}
|
||||
CHECK(marks >= 3); /* combining marks attach instead of consuming spacing */
|
||||
hb_buffer_destroy(buffer);
|
||||
hb_font_destroy(font);
|
||||
hb_face_destroy(face);
|
||||
hb_blob_destroy(blob);
|
||||
}
|
||||
|
||||
static void c4fTestBidi(void)
|
||||
{
|
||||
const char *text = "مرحبا \xE2\x81\xA8" "PS4 192.168.1.20" "\xE2\x81\xA9" " (123)";
|
||||
SBCodepointSequence sequence = {SBStringEncodingUTF8, text, strlen(text)};
|
||||
SBAlgorithmRef algorithm = SBAlgorithmCreate(&sequence);
|
||||
CHECK(algorithm);
|
||||
SBParagraphRef paragraph = SBAlgorithmCreateParagraph(algorithm, 0, sequence.stringLength,
|
||||
SBLevelDefaultRTL);
|
||||
CHECK(paragraph && SBParagraphGetBaseLevel(paragraph) == 1);
|
||||
SBLineRef line = SBParagraphCreateLine(paragraph, 0, sequence.stringLength);
|
||||
CHECK(line);
|
||||
const SBRun *runs = SBLineGetRunsPtr(line);
|
||||
SBUInteger count = SBLineGetRunCount(line);
|
||||
CHECK(count >= 3);
|
||||
unsigned latin = 0;
|
||||
for (SBUInteger i = 0; i < count; ++i) {
|
||||
if (!(runs[i].level & 1) && runs[i].length >= strlen("PS4 192.168.1.20") &&
|
||||
memcmp(text + runs[i].offset, "PS4 192.168.1.20", strlen("PS4 192.168.1.20")) == 0)
|
||||
++latin;
|
||||
}
|
||||
CHECK(latin == 1); /* isolated technical values remain LTR */
|
||||
SBLineRelease(line);
|
||||
SBParagraphRelease(paragraph);
|
||||
SBAlgorithmRelease(algorithm);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
CHECK(argc == 3);
|
||||
CHECK(hb_version_atleast(11, 2, 1));
|
||||
c4fTestFont(argv[1]);
|
||||
c4fTestFont(argv[2]);
|
||||
c4fTestBidi();
|
||||
puts("Arabic text dependencies: static fonts, joining, marks, bidi PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the pinned, offline text libraries for the launcher or host tests."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def build(out, host=False):
|
||||
out = Path(out).resolve()
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
common = ["-O2", "-fPIC", "-ffunction-sections", "-fdata-sections", "-DNDEBUG"]
|
||||
if not host:
|
||||
sdk = Path(os.environ["OO_PS4_TOOLCHAIN"])
|
||||
common += ["--target=x86_64-pc-freebsd12-elf", "-isysroot", str(sdk),
|
||||
"-isystem", str(sdk / "include")]
|
||||
specs = [
|
||||
("harfbuzz", "clang++-18", ["-std=c++17", "-fno-exceptions", "-fno-rtti",
|
||||
"-fno-threadsafe-statics", "-DHB_TINY"],
|
||||
ROOT / "vendor/harfbuzz/src/harfbuzz.cc", ROOT / "vendor/harfbuzz/src"),
|
||||
("sheenbidi", "clang-18", ["-std=c11", "-DSB_CONFIG_UNITY"],
|
||||
ROOT / "vendor/sheenbidi/Source/SheenBidi.c", ROOT / "vendor/sheenbidi/Headers"),
|
||||
]
|
||||
for name, compiler, flags, source, include in specs:
|
||||
if not host and compiler == "clang++-18":
|
||||
# OpenOrbis' cmath expects the integer abs declaration from cstdlib.
|
||||
flags = [*flags, "-isystem", str(sdk / "include/c++/v1"),
|
||||
"-include", "cstdlib"]
|
||||
obj = out / (name + ".o")
|
||||
archive = out / ("lib" + name + ".a")
|
||||
command = [compiler, *common, *flags, "-I", str(include), "-c",
|
||||
str(source), "-o", str(obj)]
|
||||
stamp = out / (name + "-command.json")
|
||||
source_dir = ROOT / "vendor" / name
|
||||
newest = max([Path(__file__).stat().st_mtime_ns,
|
||||
*(p.stat().st_mtime_ns for p in source_dir.rglob("*") if p.is_file())])
|
||||
recorded = json.dumps(command)
|
||||
if (archive.is_file() and archive.stat().st_mtime_ns >= newest and
|
||||
stamp.is_file() and stamp.read_text() == recorded):
|
||||
continue
|
||||
subprocess.run(command, check=True)
|
||||
subprocess.run(["llvm-ar-18", "rcs", str(archive), str(obj)], check=True)
|
||||
stamp.write_text(recorded)
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--host", action="store_true", help="Build for Docker host tests")
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
build(args.out, host=args.host)
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <string.h>
|
||||
#include "autorun.h"
|
||||
#include "logo.h"
|
||||
#include "language.h"
|
||||
#include "screen.h"
|
||||
#ifdef C4F_DIAG
|
||||
#include "diag.h"
|
||||
@@ -15,6 +16,13 @@
|
||||
void c4fDiagPreviewPage(int page, const char *text);
|
||||
#endif
|
||||
|
||||
int sceSystemServiceParamGetInt(int id, int *value)
|
||||
{
|
||||
(void)id;
|
||||
*value = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int c4fWrite(const char *path, const uint32_t *pixels, size_t count)
|
||||
{
|
||||
FILE *out = fopen(path, "wb");
|
||||
@@ -141,6 +149,28 @@ int main(int argc, char **argv)
|
||||
{
|
||||
if (argc == 2 && !strcmp(argv[1], "svg")) return c4fPrintSvg();
|
||||
if (argc != 4 || c4fScreenInit()) return 2;
|
||||
const char *locale = getenv("C4F_PREVIEW_LANGUAGE");
|
||||
int language = locale && !strcmp(locale, "ar") ? C4F_LANGUAGE_ARABIC :
|
||||
locale && !strcmp(locale, "pt-BR") ? C4F_LANGUAGE_PORTUGUESE : C4F_LANGUAGE_ENGLISH;
|
||||
c4fLanguageUse(language);
|
||||
/* Optional private system font fixture. Never copied into package assets. */
|
||||
const char *fontDirectory = getenv("C4F_PREVIEW_FONT_DIR");
|
||||
if (fontDirectory) {
|
||||
const char *names[2] = { "SSTArabic-Light.otf", "SSTArabic-Roman.otf" };
|
||||
unsigned char *fonts[2] = {0}; size_t sizes[2] = {0};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char path[512]; snprintf(path, sizeof(path), "%s/%s", fontDirectory, names[i]);
|
||||
FILE *file = fopen(path, "rb");
|
||||
if (!file) return 2;
|
||||
if (fseek(file, 0, SEEK_END)) return 2;
|
||||
long length = ftell(file);
|
||||
if (length <= 0 || length > 1024 * 1024 || fseek(file, 0, SEEK_SET)) return 2;
|
||||
sizes[i] = (size_t)length; fonts[i] = malloc(sizes[i]);
|
||||
if (!fonts[i] || fread(fonts[i], 1, sizes[i], file) != sizes[i]) return 2;
|
||||
fclose(file);
|
||||
}
|
||||
if (c4fTextSetArabicFonts(fonts[0], sizes[0], fonts[1], sizes[1])) return 2;
|
||||
}
|
||||
if (!strcmp(argv[1], "icon")) {
|
||||
int size = atoi(argv[2]);
|
||||
uint32_t *pixels = size > 0 && size <= 4096 ? calloc((size_t)size * size, sizeof(uint32_t)) : NULL;
|
||||
@@ -151,6 +181,7 @@ int main(int argc, char **argv)
|
||||
if (strcmp(argv[1], "screen")) return 2;
|
||||
C4fLauncherScreen s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
s.language = language; s.rtl = c4fLanguageIsArabic();
|
||||
snprintf(s.address, sizeof(s.address), "http://192.168.1.20:4264");
|
||||
const char *state = argv[2], *message = "";
|
||||
s.autorun = C4F_AUTORUN_ON;
|
||||
|
||||
@@ -24,20 +24,30 @@ SFO_VERSION = f'{int(major):02d}.{int(minor):02d}'
|
||||
|
||||
def embed_fonts():
|
||||
sources = [ROOT / 'vendor/roboto/Roboto-Light.ttf', ROOT / 'vendor/roboto/Roboto-Regular.ttf']
|
||||
sources += [ROOT / 'vendor/noto-sans-arabic/NotoSansArabic-Light.ttf', ROOT / 'vendor/noto-sans-arabic/NotoSansArabic-Regular.ttf']
|
||||
if FONTS.exists() and all(FONTS.stat().st_mtime >= s.stat().st_mtime for s in sources):
|
||||
return
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(['python3', str(ROOT / 'tools/embed_file.py'), str(FONTS),
|
||||
f'c4fFontLight={sources[0]}', f'c4fFontRegular={sources[1]}'], check=True)
|
||||
f'c4fFontLight={sources[0]}', f'c4fFontRegular={sources[1]}',
|
||||
f'c4fFontArabicLight={sources[2]}', f'c4fFontArabicRegular={sources[3]}'], check=True)
|
||||
|
||||
|
||||
def text_host_flags():
|
||||
import build_text
|
||||
out = build_text.build(ROOT / 'build/text-host', host=True)
|
||||
return ([f'-I{ROOT}/vendor/harfbuzz/src', f'-I{ROOT}/vendor/sheenbidi/Headers'],
|
||||
[str(out / 'libharfbuzz.a'), str(out / 'libsheenbidi.a')])
|
||||
|
||||
|
||||
def build_art_tool(diagnostic=False):
|
||||
"""Host build of the launcher's drawing code, for the icon and previews."""
|
||||
embed_fonts()
|
||||
text_includes, text_libraries = text_host_flags()
|
||||
art_version = VERSION + ('-diag' if diagnostic else '')
|
||||
flags = ['clang-18', '-std=gnu11', '-O2', '-D_DEFAULT_SOURCE', f'-I{ROOT}/launcher',
|
||||
f'-I{ROOT}/vendor/stb', f'-I{ROOT}/vendor/qrcodegen',
|
||||
f'-DC4F_LAUNCHER_VERSION="{art_version}"']
|
||||
f'-DC4F_LAUNCHER_VERSION="{art_version}"', *text_includes]
|
||||
extra_sources = []
|
||||
extra_flags = []
|
||||
art = ART
|
||||
@@ -53,9 +63,10 @@ def build_art_tool(diagnostic=False):
|
||||
subprocess.run([*flags, '-w', '-c', str(ROOT / 'launcher/stb_truetype.c'), '-o', str(stb)], check=True)
|
||||
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', str(ROOT / 'tools/launcher_art.c'),
|
||||
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/navigation.c'),
|
||||
str(ROOT / 'launcher/language.c'), str(ROOT / 'launcher/message.c'),
|
||||
str(ROOT / 'launcher/tester.c'), str(ROOT / 'launcher/tester_draw.c'),
|
||||
str(ROOT / 'launcher/draw.c'), str(ROOT / 'launcher/logo.c'), *extra_sources,
|
||||
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', *extra_flags, '-o', str(art)],
|
||||
str(ROOT / 'launcher/draw.c'), str(ROOT / 'launcher/text.c'), str(ROOT / 'launcher/logo.c'), *extra_sources,
|
||||
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), *text_libraries, '-lm', *extra_flags, '-o', str(art)],
|
||||
check=True)
|
||||
|
||||
|
||||
@@ -141,6 +152,10 @@ def main():
|
||||
shutil.copyfile(ROOT / 'vendor/jsmn/LICENSE', licenses / 'jsmn-MIT.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/roboto/LICENSE', licenses / 'Roboto-Apache-2.0.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/mbedtls/LICENSE', licenses / 'Mbed-TLS-LICENSE.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/harfbuzz/COPYING', licenses / 'HarfBuzz-COPYING.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/harfbuzz/src/ms-use/COPYING', licenses / 'HarfBuzz-ms-use-COPYING.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/sheenbidi/LICENSE', licenses / 'SheenBidi-Apache-2.0.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/noto-sans-arabic/OFL.txt', licenses / 'Noto-Sans-Arabic-OFL.txt')
|
||||
qrcodegen = (ROOT / 'vendor/qrcodegen/qrcodegen.h').read_text()
|
||||
(licenses / 'qrcodegen-MIT.txt').write_text(qrcodegen.split('*/', 1)[0] + '*/\n')
|
||||
stb = (ROOT / 'vendor/stb/stb_truetype.h').read_text()
|
||||
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
HarfBuzz is licensed under the so-called "Old MIT" license. Details follow.
|
||||
For parts of HarfBuzz that are licensed under different licenses see individual
|
||||
files names COPYING in subdirectories where applicable.
|
||||
|
||||
Copyright © 2010-2022 Google, Inc.
|
||||
Copyright © 2015-2020 Ebrahim Byagowi
|
||||
Copyright © 2019,2020 Facebook, Inc.
|
||||
Copyright © 2012,2015 Mozilla Foundation
|
||||
Copyright © 2011 Codethink Limited
|
||||
Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies)
|
||||
Copyright © 2009 Keith Stribley
|
||||
Copyright © 2011 Martin Hosken and SIL International
|
||||
Copyright © 2007 Chris Wilson
|
||||
Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod
|
||||
Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc.
|
||||
Copyright © 1998-2005 David Turner and Werner Lemberg
|
||||
Copyright © 2016 Igalia S.L.
|
||||
Copyright © 2022 Matthias Clasen
|
||||
Copyright © 2018,2021 Khaled Hosny
|
||||
Copyright © 2018,2019,2020 Adobe, Inc
|
||||
Copyright © 2013-2015 Alexei Podtelezhnikov
|
||||
|
||||
For full copyright notices consult the individual files in the package.
|
||||
|
||||
|
||||
Permission is hereby granted, without written agreement and without
|
||||
license or royalty fees, to use, copy, modify, and distribute this
|
||||
software and its documentation for any purpose, provided that the
|
||||
above copyright notice and the following two paragraphs appear in
|
||||
all copies of this software.
|
||||
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
||||
THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# HarfBuzz
|
||||
|
||||
Unmodified `src/` and `COPYING` from HarfBuzz **11.2.1**:
|
||||
|
||||
https://github.com/harfbuzz/harfbuzz/tree/11.2.1
|
||||
|
||||
Source archive: https://codeload.github.com/harfbuzz/harfbuzz/tar.gz/refs/tags/11.2.1
|
||||
|
||||
Archive SHA-256: `057d5754c3ac0c499bbf4d729d52acf134c7bb4ba8868ba22e84ae96bc272816`.
|
||||
|
||||
`tools/build_text.py` builds the upstream unity source with `HB_TINY`, exceptions,
|
||||
RTTI and thread-safe static guards disabled. This keeps OpenType shaping and its
|
||||
Unicode tables, including Arabic joining and positioning, without FreeType, ICU,
|
||||
fontconfig or a C++ runtime. Static TrueType and OpenType/CFF fonts use their
|
||||
OpenType layout tables; stb_truetype rasterizes their outlines separately.
|
||||
This profile does not support variable or color fonts. HarfBuzz objects belong to the drawing
|
||||
thread because `HB_TINY` disables internal locking.
|
||||
|
||||
The OpenOrbis SDK's bundled libc++ headers are used only at compile time. Its
|
||||
`cmath` header expects `cstdlib` to have declared `abs`, so the build explicitly
|
||||
includes `cstdlib` first. No vendor source or SDK patch is required.
|
||||
|
||||
License: see `COPYING` and the notices retained in the sources (including
|
||||
`src/ms-use/COPYING`).
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
#
|
||||
# Name: Legacy Simplified Arabic encoding
|
||||
#
|
||||
# Format: Three tab-separated columns
|
||||
# Column #1 is the PUA code (in hex as 0xXXXX)
|
||||
# Column #2 is the Unicode (in hex as 0xXXXX)
|
||||
# Column #3 is the Unicode name (follows a comment sign, '#')
|
||||
#
|
||||
# The entries are in PUA order
|
||||
#
|
||||
0xF100 0x063B # ARABIC LETTER KEHEH WITH TWO DOTS ABOVE
|
||||
0xF100 0x063C # ARABIC LETTER KEHEH WITH THREE DOTS BELOW
|
||||
0xF100 0x063D # ARABIC LETTER FARSI YEH WITH INVERTED V
|
||||
0xF100 0x063E # ARABIC LETTER FARSI YEH WITH TWO DOTS ABOVE
|
||||
0xF100 0x063F # ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE
|
||||
0xF100 0x0653 # ARABIC MADDAH ABOVE
|
||||
0xF100 0x0654 # ARABIC HAMZA ABOVE
|
||||
0xF100 0x0655 # ARABIC HAMZA BELOW
|
||||
0xF100 0x0656 # ARABIC SUBSCRIPT ALEF
|
||||
0xF100 0x0657 # ARABIC INVERTED DAMMA
|
||||
0xF100 0x0658 # ARABIC MARK NOON GHUNNA
|
||||
0xF100 0x0659 # ARABIC ZWARAKAY
|
||||
0xF100 0x065A # ARABIC VOWEL SIGN SMALL V ABOVE
|
||||
0xF100 0x065B # ARABIC VOWEL SIGN INVERTED SMALL V ABOVE
|
||||
0xF100 0x065C # ARABIC VOWEL SIGN DOT BELOW
|
||||
0xF100 0x065D # ARABIC REVERSED DAMMA
|
||||
0xF100 0x065E # ARABIC FATHA WITH TWO DOTS
|
||||
0xF10C 0x200C # ZERO WIDTH NON-JOINER
|
||||
0xF10D 0x200D # ZERO WIDTH JOINER
|
||||
0xF10E 0x200E # LEFT-TO-RIGHT MARK
|
||||
0xF10F 0x200F # RIGHT-TO-LEFT MARK
|
||||
0xF120 0x0020 # SPACE
|
||||
0xF121 0x0021 # EXCLAMATION MARK
|
||||
0xF122 0x0022 # QUOTATION MARK
|
||||
0xF123 0x00AB # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
|
||||
0xF124 0x00BB # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
|
||||
0xF125 0x0025 # PERCENT SIGN
|
||||
0xF126 0x00D7 # MULTIPLICATION SIGN
|
||||
0xF127 0x00F7 # DIVISION SIGN
|
||||
0xF128 0x0028 # LEFT PARENTHESIS
|
||||
0xF129 0x0029 # RIGHT PARENTHESIS
|
||||
0xF12A 0x002A # ASTERISK
|
||||
0xF12B 0x002B # PLUS SIGN
|
||||
0xF12C 0x060C # ARABIC COMMA
|
||||
0xF12D 0x002D # HYPHEN-MINUS
|
||||
0xF12E 0x002E # FULL STOP
|
||||
0xF12F 0x002F # SOLIDUS
|
||||
0xF130 0x0660 # ARABIC-INDIC DIGIT ZERO
|
||||
0xF131 0x0661 # ARABIC-INDIC DIGIT ONE
|
||||
0xF132 0x0662 # ARABIC-INDIC DIGIT TWO
|
||||
0xF133 0x0663 # ARABIC-INDIC DIGIT THREE
|
||||
0xF134 0x0664 # ARABIC-INDIC DIGIT FOUR
|
||||
0xF135 0x0665 # ARABIC-INDIC DIGIT FIVE
|
||||
0xF136 0x0666 # ARABIC-INDIC DIGIT SIX
|
||||
0xF137 0x0667 # ARABIC-INDIC DIGIT SEVEN
|
||||
0xF138 0x0668 # ARABIC-INDIC DIGIT EIGHT
|
||||
0xF139 0x0669 # ARABIC-INDIC DIGIT NINE
|
||||
0xF13A 0x003A # COLON
|
||||
0xF13B 0x003B # SEMICOLON
|
||||
0xF13B 0x061B # ARABIC SEMICOLON
|
||||
0xF13C 0x2018 # LEFT SINGLE QUOTATION MARK
|
||||
0xF13D 0x003D # EQUALS SIGN
|
||||
0xF13E 0x2019 # RIGHT SINGLE QUOTATION MARK
|
||||
0xF13F 0x003F # QUESTION MARK
|
||||
0xF13F 0x061F # ARABIC QUESTION MARK
|
||||
0xF141 0x0627 # ARABIC LETTER ALEF
|
||||
0xF141 0xFE8D # ARABIC LETTER ALEF ISOLATED FORM
|
||||
0xF142 0xFE8E # ARABIC LETTER ALEF FINAL FORM
|
||||
0xF143 0x0623 # ARABIC LETTER ALEF WITH HAMZA ABOVE
|
||||
0xF143 0xFE83 # ARABIC LETTER ALEF WITH HAMZA ABOVE ISOLATED FORM
|
||||
0xF144 0xFE84 # ARABIC LETTER ALEF WITH HAMZA ABOVE FINAL FORM
|
||||
0xF145 0x0622 # ARABIC LETTER ALEF WITH MADDA ABOVE
|
||||
0xF145 0xFE81 # ARABIC LETTER ALEF WITH MADDA ABOVE ISOLATED FORM
|
||||
0xF146 0xFE82 # ARABIC LETTER ALEF WITH MADDA ABOVE FINAL FORM
|
||||
0xF147 0x0625 # ARABIC LETTER ALEF WITH HAMZA BELOW
|
||||
0xF147 0xFE87 # ARABIC LETTER ALEF WITH HAMZA BELOW ISOLATED FORM
|
||||
0xF148 0xFE88 # ARABIC LETTER ALEF WITH HAMZA BELOW FINAL FORM
|
||||
0xF149 0xFE91 # ARABIC LETTER BEH INITIAL FORM
|
||||
0xF149 0xFE92 # ARABIC LETTER BEH MEDIAL FORM
|
||||
0xF14A 0x0628 # ARABIC LETTER BEH
|
||||
0xF14A 0xFE8F # ARABIC LETTER BEH ISOLATED FORM
|
||||
0xF14A 0xFE90 # ARABIC LETTER BEH FINAL FORM
|
||||
0xF14B 0xFE97 # ARABIC LETTER TEH INITIAL FORM
|
||||
0xF14B 0xFE98 # ARABIC LETTER TEH MEDIAL FORM
|
||||
0xF14C 0x062A # ARABIC LETTER TEH
|
||||
0xF14C 0xFE95 # ARABIC LETTER TEH ISOLATED FORM
|
||||
0xF14C 0xFE96 # ARABIC LETTER TEH FINAL FORM
|
||||
0xF14D 0xFE9B # ARABIC LETTER THEH INITIAL FORM
|
||||
0xF14D 0xFE9C # ARABIC LETTER THEH MEDIAL FORM
|
||||
0xF14E 0x062B # ARABIC LETTER THEH
|
||||
0xF14E 0xFE99 # ARABIC LETTER THEH ISOLATED FORM
|
||||
0xF14E 0xFE9A # ARABIC LETTER THEH FINAL FORM
|
||||
0xF14F 0xFE9F # ARABIC LETTER JEEM INITIAL FORM
|
||||
0xF14F 0xFEA0 # ARABIC LETTER JEEM MEDIAL FORM
|
||||
0xF150 0xFE9E # ARABIC LETTER JEEM FINAL FORM
|
||||
0xF151 0x062C # ARABIC LETTER JEEM
|
||||
0xF151 0xFE9D # ARABIC LETTER JEEM ISOLATED FORM
|
||||
0xF152 0xFEA3 # ARABIC LETTER HAH INITIAL FORM
|
||||
0xF152 0xFEA4 # ARABIC LETTER HAH MEDIAL FORM
|
||||
0xF153 0xFEA2 # ARABIC LETTER HAH FINAL FORM
|
||||
0xF154 0x062D # ARABIC LETTER HAH
|
||||
0xF154 0xFEA1 # ARABIC LETTER HAH ISOLATED FORM
|
||||
0xF155 0xFEA7 # ARABIC LETTER KHAH INITIAL FORM
|
||||
0xF155 0xFEA8 # ARABIC LETTER KHAH MEDIAL FORM
|
||||
0xF156 0xFEA6 # ARABIC LETTER KHAH FINAL FORM
|
||||
0xF157 0x062E # ARABIC LETTER KHAH
|
||||
0xF157 0xFEA5 # ARABIC LETTER KHAH ISOLATED FORM
|
||||
0xF158 0x062F # ARABIC LETTER DAL
|
||||
0xF158 0xFEA9 # ARABIC LETTER DAL ISOLATED FORM
|
||||
0xF158 0xFEAA # ARABIC LETTER DAL FINAL FORM
|
||||
0xF159 0x0630 # ARABIC LETTER THAL
|
||||
0xF159 0xFEAB # ARABIC LETTER THAL ISOLATED FORM
|
||||
0xF159 0xFEAC # ARABIC LETTER THAL FINAL FORM
|
||||
0xF15A 0x0631 # ARABIC LETTER REH
|
||||
0xF15A 0xFEAD # ARABIC LETTER REH ISOLATED FORM
|
||||
0xF15A 0xFEAE # ARABIC LETTER REH FINAL FORM
|
||||
0xF15B 0x005B # LEFT SQUARE BRACKET
|
||||
0xF15C 0x005C # REVERSE SOLIDUS
|
||||
0xF15D 0x005D # RIGHT SQUARE BRACKET
|
||||
0xF15E 0x002C # COMMA
|
||||
0xF15E 0x066B # ARABIC DECIMAL SEPARATOR
|
||||
0xF15E 0x066C # ARABIC THOUSANDS SEPARATOR
|
||||
0xF15F 0x0640 # ARABIC TATWEEL
|
||||
0xF160 0x0632 # ARABIC LETTER ZAIN
|
||||
0xF160 0xFEAF # ARABIC LETTER ZAIN ISOLATED FORM
|
||||
0xF160 0xFEB0 # ARABIC LETTER ZAIN FINAL FORM
|
||||
0xF161 0xFEB3 # ARABIC LETTER SEEN INITIAL FORM
|
||||
0xF161 0xFEB4 # ARABIC LETTER SEEN MEDIAL FORM
|
||||
0xF162 0x0633 # ARABIC LETTER SEEN
|
||||
0xF162 0xFEB1 # ARABIC LETTER SEEN ISOLATED FORM
|
||||
0xF162 0xFEB2 # ARABIC LETTER SEEN FINAL FORM
|
||||
0xF163 0xFEB7 # ARABIC LETTER SHEEN INITIAL FORM
|
||||
0xF163 0xFEB8 # ARABIC LETTER SHEEN MEDIAL FORM
|
||||
0xF164 0x0634 # ARABIC LETTER SHEEN
|
||||
0xF164 0xFEB5 # ARABIC LETTER SHEEN ISOLATED FORM
|
||||
0xF164 0xFEB6 # ARABIC LETTER SHEEN FINAL FORM
|
||||
0xF165 0xFEBB # ARABIC LETTER SAD INITIAL FORM
|
||||
0xF165 0xFEBC # ARABIC LETTER SAD MEDIAL FORM
|
||||
0xF166 0x0635 # ARABIC LETTER SAD
|
||||
0xF166 0xFEB9 # ARABIC LETTER SAD ISOLATED FORM
|
||||
0xF166 0xFEBA # ARABIC LETTER SAD FINAL FORM
|
||||
0xF167 0xFEBF # ARABIC LETTER DAD INITIAL FORM
|
||||
0xF167 0xFEC0 # ARABIC LETTER DAD MEDIAL FORM
|
||||
0xF168 0x0636 # ARABIC LETTER DAD
|
||||
0xF168 0xFEBD # ARABIC LETTER DAD ISOLATED FORM
|
||||
0xF168 0xFEBE # ARABIC LETTER DAD FINAL FORM
|
||||
0xF169 0x0637 # ARABIC LETTER TAH
|
||||
0xF169 0xFEC1 # ARABIC LETTER TAH ISOLATED FORM
|
||||
0xF169 0xFEC2 # ARABIC LETTER TAH FINAL FORM
|
||||
0xF169 0xFEC3 # ARABIC LETTER TAH INITIAL FORM
|
||||
0xF169 0xFEC4 # ARABIC LETTER TAH MEDIAL FORM
|
||||
0xF16A 0x0638 # ARABIC LETTER ZAH
|
||||
0xF16A 0xFEC5 # ARABIC LETTER ZAH ISOLATED FORM
|
||||
0xF16A 0xFEC6 # ARABIC LETTER ZAH FINAL FORM
|
||||
0xF16A 0xFEC7 # ARABIC LETTER ZAH INITIAL FORM
|
||||
0xF16A 0xFEC8 # ARABIC LETTER ZAH MEDIAL FORM
|
||||
0xF16B 0xFECB # ARABIC LETTER AIN INITIAL FORM
|
||||
0xF16C 0xFECC # ARABIC LETTER AIN MEDIAL FORM
|
||||
0xF16D 0xFECA # ARABIC LETTER AIN FINAL FORM
|
||||
0xF16E 0x0639 # ARABIC LETTER AIN
|
||||
0xF16E 0xFEC9 # ARABIC LETTER AIN ISOLATED FORM
|
||||
0xF16F 0xFECF # ARABIC LETTER GHAIN INITIAL FORM
|
||||
0xF170 0xFED0 # ARABIC LETTER GHAIN MEDIAL FORM
|
||||
0xF171 0xFECE # ARABIC LETTER GHAIN FINAL FORM
|
||||
0xF172 0x063A # ARABIC LETTER GHAIN
|
||||
0xF172 0xFECD # ARABIC LETTER GHAIN ISOLATED FORM
|
||||
0xF173 0xFED3 # ARABIC LETTER FEH INITIAL FORM
|
||||
0xF174 0xFED4 # ARABIC LETTER FEH MEDIAL FORM
|
||||
0xF175 0x0641 # ARABIC LETTER FEH
|
||||
0xF175 0xFED1 # ARABIC LETTER FEH ISOLATED FORM
|
||||
0xF175 0xFED2 # ARABIC LETTER FEH FINAL FORM
|
||||
0xF176 0xFED7 # ARABIC LETTER QAF INITIAL FORM
|
||||
0xF177 0xFED8 # ARABIC LETTER QAF MEDIAL FORM
|
||||
0xF178 0x0642 # ARABIC LETTER QAF
|
||||
0xF178 0xFED5 # ARABIC LETTER QAF ISOLATED FORM
|
||||
0xF178 0xFED6 # ARABIC LETTER QAF FINAL FORM
|
||||
0xF179 0xFEDB # ARABIC LETTER KAF INITIAL FORM
|
||||
0xF179 0xFEDC # ARABIC LETTER KAF MEDIAL FORM
|
||||
0xF17A 0x0643 # ARABIC LETTER KAF
|
||||
0xF17A 0xFED9 # ARABIC LETTER KAF ISOLATED FORM
|
||||
0xF17A 0xFEDA # ARABIC LETTER KAF FINAL FORM
|
||||
0xF17B 0xFEDF # ARABIC LETTER LAM INITIAL FORM
|
||||
0xF17B 0xFEE0 # ARABIC LETTER LAM MEDIAL FORM
|
||||
0xF17C 0x0644 # ARABIC LETTER LAM
|
||||
0xF17C 0xFEDD # ARABIC LETTER LAM ISOLATED FORM
|
||||
0xF17C 0xFEDE # ARABIC LETTER LAM FINAL FORM
|
||||
0xF17D 0xFEE3 # ARABIC LETTER MEEM INITIAL FORM
|
||||
0xF17D 0xFEE4 # ARABIC LETTER MEEM MEDIAL FORM
|
||||
0xF17E 0x0645 # ARABIC LETTER MEEM
|
||||
0xF17E 0xFEE1 # ARABIC LETTER MEEM ISOLATED FORM
|
||||
0xF17E 0xFEE2 # ARABIC LETTER MEEM FINAL FORM
|
||||
0xF17F 0xFEE7 # ARABIC LETTER NOON INITIAL FORM
|
||||
0xF17F 0xFEE8 # ARABIC LETTER NOON MEDIAL FORM
|
||||
0xF1A1 0xFEEB # ARABIC LETTER HEH INITIAL FORM
|
||||
0xF1A2 0xFEEC # ARABIC LETTER HEH MEDIAL FORM
|
||||
0xF1A3 0xFEEA # ARABIC LETTER HEH FINAL FORM
|
||||
0xF1A4 0x0647 # ARABIC LETTER HEH
|
||||
0xF1A4 0xFEE9 # ARABIC LETTER HEH ISOLATED FORM
|
||||
0xF1A5 0x0648 # ARABIC LETTER WAW
|
||||
0xF1A5 0xFEED # ARABIC LETTER WAW ISOLATED FORM
|
||||
0xF1A5 0xFEEE # ARABIC LETTER WAW FINAL FORM
|
||||
0xF1A6 0xFEF3 # ARABIC LETTER YEH INITIAL FORM
|
||||
0xF1A6 0xFEF4 # ARABIC LETTER YEH MEDIAL FORM
|
||||
0xF1A7 0xFEF2 # ARABIC LETTER YEH FINAL FORM
|
||||
0xF1A8 0x064A # ARABIC LETTER YEH
|
||||
0xF1A8 0xFEF1 # ARABIC LETTER YEH ISOLATED FORM
|
||||
0xF1A9 0x0629 # ARABIC LETTER TEH MARBUTA
|
||||
0xF1A9 0xFE93 # ARABIC LETTER TEH MARBUTA ISOLATED FORM
|
||||
0xF1AA 0xFE94 # ARABIC LETTER TEH MARBUTA FINAL FORM
|
||||
0xF1AB 0xFEF0 # ARABIC LETTER ALEF MAKSURA FINAL FORM
|
||||
0xF1AC 0x0649 # ARABIC LETTER ALEF MAKSURA
|
||||
0xF1AC 0xFEEF # ARABIC LETTER ALEF MAKSURA ISOLATED FORM
|
||||
0xF1AD 0x0621 # ARABIC LETTER HAMZA
|
||||
0xF1AE 0xFE8B # ARABIC LETTER YEH WITH HAMZA ABOVE INITIAL FORM
|
||||
0xF1AE 0xFE8C # ARABIC LETTER YEH WITH HAMZA ABOVE MEDIAL FORM
|
||||
0xF1AF 0xFE8A # ARABIC LETTER YEH WITH HAMZA ABOVE FINAL FORM
|
||||
0xF1B0 0x0030 # DIGIT ZERO
|
||||
0xF1B1 0x0031 # DIGIT ONE
|
||||
0xF1B2 0x0032 # DIGIT TWO
|
||||
0xF1B3 0x0033 # DIGIT THREE
|
||||
0xF1B4 0x0034 # DIGIT FOUR
|
||||
0xF1B5 0x0035 # DIGIT FIVE
|
||||
0xF1B6 0x0036 # DIGIT SIX
|
||||
0xF1B7 0x0037 # DIGIT SEVEN
|
||||
0xF1B8 0x0038 # DIGIT EIGHT
|
||||
0xF1B9 0x0039 # DIGIT NINE
|
||||
0xF1BA 0x0626 # ARABIC LETTER YEH WITH HAMZA ABOVE
|
||||
0xF1BA 0xFE89 # ARABIC LETTER YEH WITH HAMZA ABOVE ISOLATED FORM
|
||||
0xF1BB 0x0624 # ARABIC LETTER WAW WITH HAMZA ABOVE
|
||||
0xF1BB 0xFE85 # ARABIC LETTER WAW WITH HAMZA ABOVE ISOLATED FORM
|
||||
0xF1BB 0xFE86 # ARABIC LETTER WAW WITH HAMZA ABOVE FINAL FORM
|
||||
0xF1BC 0xFEFC # ARABIC LIGATURE LAM WITH ALEF FINAL FORM
|
||||
0xF1BD 0xFEFB # ARABIC LIGATURE LAM WITH ALEF ISOLATED FORM
|
||||
0xF1BE 0xFEF7 # ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE ISOLATED FORM
|
||||
0xF1BF 0xFEF8 # ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE FINAL FORM
|
||||
0xF1C0 0xFEF5 # ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE ISOLATED FORM
|
||||
0xF1C1 0xFEF6 # ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE FINAL FORM
|
||||
0xF1C2 0xFEF9 # ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW ISOLATED FORM
|
||||
0xF1C3 0xFEFA # ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW FINAL FORM
|
||||
0xF1C4 0x064E # ARABIC FATHA
|
||||
0xF1C5 0x064F # ARABIC DAMMA
|
||||
0xF1C6 0x0652 # ARABIC SUKUN
|
||||
0xF1C7 0x064B # ARABIC FATHATAN
|
||||
0xF1C8 0x064C # ARABIC DAMMATAN
|
||||
0xF1C9 0x0651 # ARABIC SHADDA
|
||||
0xF1CA 0x0650 # ARABIC KASRA
|
||||
0xF1CB 0x064D # ARABIC KASRATAN
|
||||
0xF1E1 0x0646 # ARABIC LETTER NOON
|
||||
0xF1E1 0xFEE5 # ARABIC LETTER NOON ISOLATED FORM
|
||||
0xF1E1 0xFEE6 # ARABIC LETTER NOON FINAL FORM
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
#
|
||||
# Name: Legacy Traditional Arabic encoding
|
||||
#
|
||||
# Format: Three tab-separated columns
|
||||
# Column #1 is the PUA code (in hex as 0xXXXX)
|
||||
# Column #2 is the Unicode (in hex as 0xXXXX)
|
||||
# Column #3 is the Unicode name (follows a comment sign, '#')
|
||||
#
|
||||
# The entries are in PUA order
|
||||
#
|
||||
0xF200 0x063B # ARABIC LETTER KEHEH WITH TWO DOTS ABOVE
|
||||
0xF200 0x063C # ARABIC LETTER KEHEH WITH THREE DOTS BELOW
|
||||
0xF200 0x063D # ARABIC LETTER FARSI YEH WITH INVERTED V
|
||||
0xF200 0x063E # ARABIC LETTER FARSI YEH WITH TWO DOTS ABOVE
|
||||
0xF200 0x063F # ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE
|
||||
0xF200 0x0653 # ARABIC MADDAH ABOVE
|
||||
0xF200 0x0654 # ARABIC HAMZA ABOVE
|
||||
0xF200 0x0655 # ARABIC HAMZA BELOW
|
||||
0xF200 0x0656 # ARABIC SUBSCRIPT ALEF
|
||||
0xF200 0x0657 # ARABIC INVERTED DAMMA
|
||||
0xF200 0x0658 # ARABIC MARK NOON GHUNNA
|
||||
0xF200 0x0659 # ARABIC ZWARAKAY
|
||||
0xF200 0x065A # ARABIC VOWEL SIGN SMALL V ABOVE
|
||||
0xF200 0x065B # ARABIC VOWEL SIGN INVERTED SMALL V ABOVE
|
||||
0xF200 0x065C # ARABIC VOWEL SIGN DOT BELOW
|
||||
0xF200 0x065D # ARABIC REVERSED DAMMA
|
||||
0xF200 0x065E # ARABIC FATHA WITH TWO DOTS
|
||||
0xF202 0xFC08 # ARABIC LIGATURE BEH WITH MEEM ISOLATED FORM
|
||||
0xF203 0xFC0E # ARABIC LIGATURE TEH WITH MEEM ISOLATED FORM
|
||||
0xF204 0xFC12 # ARABIC LIGATURE THEH WITH MEEM ISOLATED FORM
|
||||
0xF205 0xFC42 # ARABIC LIGATURE LAM WITH MEEM ISOLATED FORM
|
||||
0xF206 0xFC4E # ARABIC LIGATURE NOON WITH MEEM ISOLATED FORM
|
||||
0xF20C 0x200C # ZERO WIDTH NON-JOINER
|
||||
0xF20D 0x200D # ZERO WIDTH JOINER
|
||||
0xF20E 0x200E # LEFT-TO-RIGHT MARK
|
||||
0xF20F 0x200F # RIGHT-TO-LEFT MARK
|
||||
0xF210 0xFD88 # ARABIC LIGATURE LAM WITH MEEM WITH HAH INITIAL FORM
|
||||
0xF212 0xFC3F # ARABIC LIGATURE LAM WITH JEEM ISOLATED FORM
|
||||
0xF213 0xFC40 # ARABIC LIGATURE LAM WITH HAH ISOLATED FORM
|
||||
0xF214 0xFC41 # ARABIC LIGATURE LAM WITH KHAH ISOLATED FORM
|
||||
0xF215 0xFC6A # ARABIC LIGATURE BEH WITH REH FINAL FORM
|
||||
0xF216 0xFC70 # ARABIC LIGATURE TEH WITH REH FINAL FORM
|
||||
0xF217 0xFC91 # ARABIC LIGATURE YEH WITH REH FINAL FORM
|
||||
0xF218 0xFCB0 # ARABIC LIGATURE SEEN WITH MEEM INITIAL FORM
|
||||
0xF219 0xFD30 # ARABIC LIGATURE SHEEN WITH MEEM INITIAL FORM
|
||||
0xF21A 0xFCCD # ARABIC LIGATURE LAM WITH HEH INITIAL FORM
|
||||
0xF21C 0xFC44 # ARABIC LIGATURE LAM WITH YEH ISOLATED FORM
|
||||
0xF21D 0xFC0A # ARABIC LIGATURE BEH WITH YEH ISOLATED FORM
|
||||
0xF21E 0xFC10 # ARABIC LIGATURE TEH WITH YEH ISOLATED FORM
|
||||
0xF21F 0xFC50 # ARABIC LIGATURE NOON WITH YEH ISOLATED FORM
|
||||
0xF220 0x0020 # SPACE
|
||||
0xF221 0x0021 # EXCLAMATION MARK
|
||||
0xF222 0x0022 # QUOTATION MARK
|
||||
0xF223 0x00AB # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
|
||||
0xF224 0x00BB # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
|
||||
0xF225 0x0025 # PERCENT SIGN
|
||||
0xF226 0x00D7 # MULTIPLICATION SIGN
|
||||
0xF227 0x00F7 # DIVISION SIGN
|
||||
0xF228 0x0028 # LEFT PARENTHESIS
|
||||
0xF229 0x0029 # RIGHT PARENTHESIS
|
||||
0xF22A 0x002A # ASTERISK
|
||||
0xF22B 0x002B # PLUS SIGN
|
||||
0xF22C 0x060C # ARABIC COMMA
|
||||
0xF22D 0x002D # HYPHEN-MINUS
|
||||
0xF22E 0x002E # FULL STOP
|
||||
0xF22F 0x002F # SOLIDUS
|
||||
0xF230 0x0660 # ARABIC-INDIC DIGIT ZERO
|
||||
0xF231 0x0661 # ARABIC-INDIC DIGIT ONE
|
||||
0xF232 0x0662 # ARABIC-INDIC DIGIT TWO
|
||||
0xF233 0x0663 # ARABIC-INDIC DIGIT THREE
|
||||
0xF234 0x0664 # ARABIC-INDIC DIGIT FOUR
|
||||
0xF235 0x0665 # ARABIC-INDIC DIGIT FIVE
|
||||
0xF236 0x0666 # ARABIC-INDIC DIGIT SIX
|
||||
0xF237 0x0667 # ARABIC-INDIC DIGIT SEVEN
|
||||
0xF238 0x0668 # ARABIC-INDIC DIGIT EIGHT
|
||||
0xF239 0x0669 # ARABIC-INDIC DIGIT NINE
|
||||
0xF23A 0x003A # COLON
|
||||
0xF23B 0x003B # SEMICOLON
|
||||
0xF23B 0x061B # ARABIC SEMICOLON
|
||||
0xF23C 0x201C # LEFT DOUBLE QUOTATION MARK
|
||||
0xF23D 0x003D # EQUALS SIGN
|
||||
0xF23E 0x201D # RIGHT DOUBLE QUOTATION MARK
|
||||
0xF23F 0x003F # QUESTION MARK
|
||||
0xF23F 0x061F # ARABIC QUESTION MARK
|
||||
0xF241 0x0627 # ARABIC LETTER ALEF
|
||||
0xF241 0xFE8D # ARABIC LETTER ALEF ISOLATED FORM
|
||||
0xF242 0xFE8E # ARABIC LETTER ALEF FINAL FORM
|
||||
0xF243 0x0623 # ARABIC LETTER ALEF WITH HAMZA ABOVE
|
||||
0xF243 0xFE83 # ARABIC LETTER ALEF WITH HAMZA ABOVE ISOLATED FORM
|
||||
0xF244 0xFE84 # ARABIC LETTER ALEF WITH HAMZA ABOVE FINAL FORM
|
||||
0xF245 0x0622 # ARABIC LETTER ALEF WITH MADDA ABOVE
|
||||
0xF245 0xFE81 # ARABIC LETTER ALEF WITH MADDA ABOVE ISOLATED FORM
|
||||
0xF246 0xFE82 # ARABIC LETTER ALEF WITH MADDA ABOVE FINAL FORM
|
||||
0xF247 0x0625 # ARABIC LETTER ALEF WITH HAMZA BELOW
|
||||
0xF247 0xFE87 # ARABIC LETTER ALEF WITH HAMZA BELOW ISOLATED FORM
|
||||
0xF248 0xFE88 # ARABIC LETTER ALEF WITH HAMZA BELOW FINAL FORM
|
||||
0xF249 0xFE91 # ARABIC LETTER BEH INITIAL FORM
|
||||
0xF24A 0xFE92 # ARABIC LETTER BEH MEDIAL FORM
|
||||
0xF24B 0xFE90 # ARABIC LETTER BEH FINAL FORM
|
||||
0xF24C 0x0628 # ARABIC LETTER BEH
|
||||
0xF24C 0xFE8F # ARABIC LETTER BEH ISOLATED FORM
|
||||
0xF24D 0xFE97 # ARABIC LETTER TEH INITIAL FORM
|
||||
0xF24E 0xFE98 # ARABIC LETTER TEH MEDIAL FORM
|
||||
0xF24F 0xFE96 # ARABIC LETTER TEH FINAL FORM
|
||||
0xF250 0x062A # ARABIC LETTER TEH
|
||||
0xF250 0xFE95 # ARABIC LETTER TEH ISOLATED FORM
|
||||
0xF251 0xFE9B # ARABIC LETTER THEH INITIAL FORM
|
||||
0xF252 0xFE9C # ARABIC LETTER THEH MEDIAL FORM
|
||||
0xF253 0xFE9A # ARABIC LETTER THEH FINAL FORM
|
||||
0xF254 0x062B # ARABIC LETTER THEH
|
||||
0xF254 0xFE99 # ARABIC LETTER THEH ISOLATED FORM
|
||||
0xF255 0xFE9F # ARABIC LETTER JEEM INITIAL FORM
|
||||
0xF256 0xFEA0 # ARABIC LETTER JEEM MEDIAL FORM
|
||||
0xF257 0xFE9E # ARABIC LETTER JEEM FINAL FORM
|
||||
0xF258 0x062C # ARABIC LETTER JEEM
|
||||
0xF258 0xFE9D # ARABIC LETTER JEEM ISOLATED FORM
|
||||
0xF259 0xFEA3 # ARABIC LETTER HAH INITIAL FORM
|
||||
0xF25A 0xFEA4 # ARABIC LETTER HAH MEDIAL FORM
|
||||
0xF25B 0x005B # LEFT SQUARE BRACKET
|
||||
0xF25C 0xFEA2 # ARABIC LETTER HAH FINAL FORM
|
||||
0xF25D 0x005D # RIGHT SQUARE BRACKET
|
||||
0xF25E 0x002C # COMMA
|
||||
0xF25E 0x066B # ARABIC DECIMAL SEPARATOR
|
||||
0xF25E 0x066C # ARABIC THOUSANDS SEPARATOR
|
||||
0xF25F 0x0640 # ARABIC TATWEEL
|
||||
0xF260 0x062D # ARABIC LETTER HAH
|
||||
0xF260 0xFEA1 # ARABIC LETTER HAH ISOLATED FORM
|
||||
0xF261 0xFEA7 # ARABIC LETTER KHAH INITIAL FORM
|
||||
0xF262 0xFEA8 # ARABIC LETTER KHAH MEDIAL FORM
|
||||
0xF263 0xFEA6 # ARABIC LETTER KHAH FINAL FORM
|
||||
0xF264 0x062E # ARABIC LETTER KHAH
|
||||
0xF264 0xFEA5 # ARABIC LETTER KHAH ISOLATED FORM
|
||||
0xF265 0x062F # ARABIC LETTER DAL
|
||||
0xF265 0xFEA9 # ARABIC LETTER DAL ISOLATED FORM
|
||||
0xF266 0xFEAA # ARABIC LETTER DAL FINAL FORM
|
||||
0xF267 0x0630 # ARABIC LETTER THAL
|
||||
0xF267 0xFEAB # ARABIC LETTER THAL ISOLATED FORM
|
||||
0xF268 0xFEAC # ARABIC LETTER THAL FINAL FORM
|
||||
0xF269 0x0631 # ARABIC LETTER REH
|
||||
0xF269 0xFEAD # ARABIC LETTER REH ISOLATED FORM
|
||||
0xF26A 0xFEAE # ARABIC LETTER REH FINAL FORM
|
||||
0xF26B 0x0632 # ARABIC LETTER ZAIN
|
||||
0xF26B 0xFEAF # ARABIC LETTER ZAIN ISOLATED FORM
|
||||
0xF26C 0xFEB0 # ARABIC LETTER ZAIN FINAL FORM
|
||||
0xF26D 0xFEB3 # ARABIC LETTER SEEN INITIAL FORM
|
||||
0xF26E 0xFEB4 # ARABIC LETTER SEEN MEDIAL FORM
|
||||
0xF26F 0xFEB2 # ARABIC LETTER SEEN FINAL FORM
|
||||
0xF270 0x0633 # ARABIC LETTER SEEN
|
||||
0xF270 0xFEB1 # ARABIC LETTER SEEN ISOLATED FORM
|
||||
0xF271 0xFEB7 # ARABIC LETTER SHEEN INITIAL FORM
|
||||
0xF272 0xFEB8 # ARABIC LETTER SHEEN MEDIAL FORM
|
||||
0xF273 0xFEB6 # ARABIC LETTER SHEEN FINAL FORM
|
||||
0xF274 0x0634 # ARABIC LETTER SHEEN
|
||||
0xF274 0xFEB5 # ARABIC LETTER SHEEN ISOLATED FORM
|
||||
0xF275 0xFEBB # ARABIC LETTER SAD INITIAL FORM
|
||||
0xF276 0xFEBC # ARABIC LETTER SAD MEDIAL FORM
|
||||
0xF277 0xFEBA # ARABIC LETTER SAD FINAL FORM
|
||||
0xF278 0x0635 # ARABIC LETTER SAD
|
||||
0xF278 0xFEB9 # ARABIC LETTER SAD ISOLATED FORM
|
||||
0xF279 0xFEBF # ARABIC LETTER DAD INITIAL FORM
|
||||
0xF27A 0xFEC0 # ARABIC LETTER DAD MEDIAL FORM
|
||||
0xF27B 0xFD3E # ORNATE LEFT PARENTHESIS
|
||||
0xF27C 0xFEBE # ARABIC LETTER DAD FINAL FORM
|
||||
0xF27D 0xFD3F # ORNATE RIGHT PARENTHESIS
|
||||
0xF27E 0x0636 # ARABIC LETTER DAD
|
||||
0xF27E 0xFEBD # ARABIC LETTER DAD ISOLATED FORM
|
||||
0xF27F 0xFEC3 # ARABIC LETTER TAH INITIAL FORM
|
||||
0xF280 0xFC9C # ARABIC LIGATURE BEH WITH JEEM INITIAL FORM
|
||||
0xF281 0xFC9D # ARABIC LIGATURE BEH WITH HAH INITIAL FORM
|
||||
0xF282 0xFC9E # ARABIC LIGATURE BEH WITH KHAH INITIAL FORM
|
||||
0xF283 0xFCA1 # ARABIC LIGATURE TEH WITH JEEM INITIAL FORM
|
||||
0xF284 0xFCA2 # ARABIC LIGATURE TEH WITH HAH INITIAL FORM
|
||||
0xF285 0xFCA3 # ARABIC LIGATURE TEH WITH KHAH INITIAL FORM
|
||||
0xF286 0xFCC9 # ARABIC LIGATURE LAM WITH JEEM INITIAL FORM
|
||||
0xF287 0xFCCA # ARABIC LIGATURE LAM WITH HAH INITIAL FORM
|
||||
0xF288 0xFCCB # ARABIC LIGATURE LAM WITH KHAH INITIAL FORM
|
||||
0xF289 0xFCCE # ARABIC LIGATURE MEEM WITH JEEM INITIAL FORM
|
||||
0xF28A 0xFCCF # ARABIC LIGATURE MEEM WITH HAH INITIAL FORM
|
||||
0xF28B 0xFCD0 # ARABIC LIGATURE MEEM WITH KHAH INITIAL FORM
|
||||
0xF28D 0xFCD2 # ARABIC LIGATURE NOON WITH JEEM INITIAL FORM
|
||||
0xF28E 0xFCD3 # ARABIC LIGATURE NOON WITH HAH INITIAL FORM
|
||||
0xF28F 0xFCDA # ARABIC LIGATURE YEH WITH JEEM INITIAL FORM
|
||||
0xF290 0xFCDB # ARABIC LIGATURE YEH WITH HAH INITIAL FORM
|
||||
0xF291 0xFCDC # ARABIC LIGATURE YEH WITH KHAH INITIAL FORM
|
||||
0xF292 0xFC6D # ARABIC LIGATURE BEH WITH NOON FINAL FORM
|
||||
0xF293 0xFC73 # ARABIC LIGATURE TEH WITH NOON FINAL FORM
|
||||
0xF294 0xFC94 # ARABIC LIGATURE YEH WITH NOON FINAL FORM
|
||||
0xF295 0xFC86 # ARABIC LIGATURE LAM WITH ALEF MAKSURA FINAL FORM
|
||||
0xF296 0xFC9F # ARABIC LIGATURE BEH WITH MEEM INITIAL FORM
|
||||
0xF297 0xFCA4 # ARABIC LIGATURE TEH WITH MEEM INITIAL FORM
|
||||
0xF298 0xFCD5 # ARABIC LIGATURE NOON WITH MEEM INITIAL FORM
|
||||
0xF299 0xFCDD # ARABIC LIGATURE YEH WITH MEEM INITIAL FORM
|
||||
0xF29A 0xFCA8 # ARABIC LIGATURE JEEM WITH MEEM INITIAL FORM
|
||||
0xF29B 0xFCAA # ARABIC LIGATURE HAH WITH MEEM INITIAL FORM
|
||||
0xF29C 0xFCAC # ARABIC LIGATURE KHAH WITH MEEM INITIAL FORM
|
||||
0xF29D 0xFCCC # ARABIC LIGATURE LAM WITH MEEM INITIAL FORM
|
||||
0xF29E 0xFCD1 # ARABIC LIGATURE MEEM WITH MEEM INITIAL FORM
|
||||
0xF29F 0xFC32 # ARABIC LIGATURE FEH WITH YEH ISOLATED FORM
|
||||
0xF2A1 0xFEC2 # ARABIC LETTER TAH FINAL FORM
|
||||
0xF2A2 0x0637 # ARABIC LETTER TAH
|
||||
0xF2A2 0xFEC1 # ARABIC LETTER TAH ISOLATED FORM
|
||||
0xF2A3 0x0638 # ARABIC LETTER ZAH
|
||||
0xF2A3 0xFEC7 # ARABIC LETTER ZAH INITIAL FORM
|
||||
0xF2A4 0xFEC8 # ARABIC LETTER ZAH MEDIAL FORM
|
||||
0xF2A5 0xFEC6 # ARABIC LETTER ZAH FINAL FORM
|
||||
0xF2A6 0xFEC5 # ARABIC LETTER ZAH ISOLATED FORM
|
||||
0xF2A7 0xFECB # ARABIC LETTER AIN INITIAL FORM
|
||||
0xF2A8 0xFECC # ARABIC LETTER AIN MEDIAL FORM
|
||||
0xF2A9 0xFECA # ARABIC LETTER AIN FINAL FORM
|
||||
0xF2AA 0x0639 # ARABIC LETTER AIN
|
||||
0xF2AA 0xFEC9 # ARABIC LETTER AIN ISOLATED FORM
|
||||
0xF2AB 0xFECF # ARABIC LETTER GHAIN INITIAL FORM
|
||||
0xF2AC 0xFED0 # ARABIC LETTER GHAIN MEDIAL FORM
|
||||
0xF2AD 0xFECE # ARABIC LETTER GHAIN FINAL FORM
|
||||
0xF2AE 0x063A # ARABIC LETTER GHAIN
|
||||
0xF2AE 0xFECD # ARABIC LETTER GHAIN ISOLATED FORM
|
||||
0xF2AF 0xFED3 # ARABIC LETTER FEH INITIAL FORM
|
||||
0xF2B0 0xFED4 # ARABIC LETTER FEH MEDIAL FORM
|
||||
0xF2B1 0xFED2 # ARABIC LETTER FEH FINAL FORM
|
||||
0xF2B2 0x0641 # ARABIC LETTER FEH
|
||||
0xF2B2 0xFED1 # ARABIC LETTER FEH ISOLATED FORM
|
||||
0xF2B3 0xFED7 # ARABIC LETTER QAF INITIAL FORM
|
||||
0xF2B4 0xFED8 # ARABIC LETTER QAF MEDIAL FORM
|
||||
0xF2B5 0xFED6 # ARABIC LETTER QAF FINAL FORM
|
||||
0xF2B6 0x0642 # ARABIC LETTER QAF
|
||||
0xF2B6 0xFED5 # ARABIC LETTER QAF ISOLATED FORM
|
||||
0xF2B7 0xFEDB # ARABIC LETTER KAF INITIAL FORM
|
||||
0xF2B8 0xFEDC # ARABIC LETTER KAF MEDIAL FORM
|
||||
0xF2B9 0xFEDA # ARABIC LETTER KAF FINAL FORM
|
||||
0xF2BA 0x0643 # ARABIC LETTER KAF
|
||||
0xF2BA 0xFED9 # ARABIC LETTER KAF ISOLATED FORM
|
||||
0xF2BB 0xFEDF # ARABIC LETTER LAM INITIAL FORM
|
||||
0xF2BC 0xFEE0 # ARABIC LETTER LAM MEDIAL FORM
|
||||
0xF2BD 0xFEDE # ARABIC LETTER LAM FINAL FORM
|
||||
0xF2BE 0x0644 # ARABIC LETTER LAM
|
||||
0xF2BE 0xFEDD # ARABIC LETTER LAM ISOLATED FORM
|
||||
0xF2BF 0xFEE3 # ARABIC LETTER MEEM INITIAL FORM
|
||||
0xF2C0 0xFEE4 # ARABIC LETTER MEEM MEDIAL FORM
|
||||
0xF2C1 0xFEE2 # ARABIC LETTER MEEM FINAL FORM
|
||||
0xF2C2 0x0645 # ARABIC LETTER MEEM
|
||||
0xF2C2 0xFEE1 # ARABIC LETTER MEEM ISOLATED FORM
|
||||
0xF2C3 0xFEE7 # ARABIC LETTER NOON INITIAL FORM
|
||||
0xF2C4 0xFEE8 # ARABIC LETTER NOON MEDIAL FORM
|
||||
0xF2C5 0xFEE6 # ARABIC LETTER NOON FINAL FORM
|
||||
0xF2C6 0x0646 # ARABIC LETTER NOON
|
||||
0xF2C6 0xFEE5 # ARABIC LETTER NOON ISOLATED FORM
|
||||
0xF2C7 0xFEEB # ARABIC LETTER HEH INITIAL FORM
|
||||
0xF2C8 0xFEEC # ARABIC LETTER HEH MEDIAL FORM
|
||||
0xF2C9 0xFEEA # ARABIC LETTER HEH FINAL FORM
|
||||
0xF2CA 0x0647 # ARABIC LETTER HEH
|
||||
0xF2CA 0xFEE9 # ARABIC LETTER HEH ISOLATED FORM
|
||||
0xF2CB 0x0648 # ARABIC LETTER WAW
|
||||
0xF2CB 0xFEED # ARABIC LETTER WAW ISOLATED FORM
|
||||
0xF2CC 0xFEEE # ARABIC LETTER WAW FINAL FORM
|
||||
0xF2CD 0xFEF3 # ARABIC LETTER YEH INITIAL FORM
|
||||
0xF2CE 0xFEF4 # ARABIC LETTER YEH MEDIAL FORM
|
||||
0xF2CF 0xFEF2 # ARABIC LETTER YEH FINAL FORM
|
||||
0xF2D0 0x064A # ARABIC LETTER YEH
|
||||
0xF2D0 0xFEF1 # ARABIC LETTER YEH ISOLATED FORM
|
||||
0xF2D1 0x0629 # ARABIC LETTER TEH MARBUTA
|
||||
0xF2D1 0xFE93 # ARABIC LETTER TEH MARBUTA ISOLATED FORM
|
||||
0xF2D2 0xFE94 # ARABIC LETTER TEH MARBUTA FINAL FORM
|
||||
0xF2D3 0xFEF0 # ARABIC LETTER ALEF MAKSURA FINAL FORM
|
||||
0xF2D4 0x0649 # ARABIC LETTER ALEF MAKSURA
|
||||
0xF2D4 0xFEEF # ARABIC LETTER ALEF MAKSURA ISOLATED FORM
|
||||
0xF2D5 0x0621 # ARABIC LETTER HAMZA
|
||||
0xF2D6 0xFE8B # ARABIC LETTER YEH WITH HAMZA ABOVE INITIAL FORM
|
||||
0xF2D7 0xFE8C # ARABIC LETTER YEH WITH HAMZA ABOVE MEDIAL FORM
|
||||
0xF2D8 0xFE8A # ARABIC LETTER YEH WITH HAMZA ABOVE FINAL FORM
|
||||
0xF2D9 0x0626 # ARABIC LETTER YEH WITH HAMZA ABOVE
|
||||
0xF2D9 0xFE89 # ARABIC LETTER YEH WITH HAMZA ABOVE ISOLATED FORM
|
||||
0xF2DA 0x0624 # ARABIC LETTER WAW WITH HAMZA ABOVE
|
||||
0xF2DA 0xFE85 # ARABIC LETTER WAW WITH HAMZA ABOVE ISOLATED FORM
|
||||
0xF2DB 0xFE86 # ARABIC LETTER WAW WITH HAMZA ABOVE FINAL FORM
|
||||
0xF2DC 0xFEFB # ARABIC LIGATURE LAM WITH ALEF ISOLATED FORM
|
||||
0xF2DD 0xFEFC # ARABIC LIGATURE LAM WITH ALEF FINAL FORM
|
||||
0xF2DE 0xFEF7 # ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE ISOLATED FORM
|
||||
0xF2DF 0xFEF8 # ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE FINAL FORM
|
||||
0xF2E0 0xFEF5 # ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE ISOLATED FORM
|
||||
0xF2E1 0xFEF6 # ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE FINAL FORM
|
||||
0xF2E2 0xFEF9 # ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW ISOLATED FORM
|
||||
0xF2E3 0xFEFA # ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW FINAL FORM
|
||||
0xF2E4 0x064E # ARABIC FATHA
|
||||
0xF2E5 0x064F # ARABIC DAMMA
|
||||
0xF2E6 0x0652 # ARABIC SUKUN
|
||||
0xF2E7 0x064B # ARABIC FATHATAN
|
||||
0xF2E8 0x064C # ARABIC DAMMATAN
|
||||
0xF2E9 0x0651 # ARABIC SHADDA
|
||||
0xF2EA 0x0650 # ARABIC KASRA
|
||||
0xF2EB 0x064D # ARABIC KASRATAN
|
||||
0xF2EC 0xFC60 # ARABIC LIGATURE SHADDA WITH FATHA ISOLATED FORM
|
||||
0xF2ED 0xFC61 # ARABIC LIGATURE SHADDA WITH DAMMA ISOLATED FORM
|
||||
0xF2EF 0xFC5E # ARABIC LIGATURE SHADDA WITH DAMMATAN ISOLATED FORM
|
||||
0xF2F0 0xFC62 # ARABIC LIGATURE SHADDA WITH KASRA ISOLATED FORM
|
||||
0xF2F1 0xFEC4 # ARABIC LETTER TAH MEDIAL FORM
|
||||
+1033
File diff suppressed because it is too large.
Load diff
+2865
File diff suppressed because it is too large.
Load diff
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright © 2018 Ebrahim Byagowi
|
||||
* Copyright © 2020 Google, Inc.
|
||||
*
|
||||
* This is part of HarfBuzz, a text shaping library.
|
||||
*
|
||||
* Permission is hereby granted, without written agreement and without
|
||||
* license or royalty fees, to use, copy, modify, and distribute this
|
||||
* software and its documentation for any purpose, provided that the
|
||||
* above copyright notice and the following two paragraphs appear in
|
||||
* all copies of this software.
|
||||
*
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*
|
||||
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef OT_COLOR_COLR_COLRV1_CLOSURE_HH
|
||||
#define OT_COLOR_COLR_COLRV1_CLOSURE_HH
|
||||
|
||||
#include "../../../hb-open-type.hh"
|
||||
#include "COLR.hh"
|
||||
|
||||
/*
|
||||
* COLR -- Color
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/colr
|
||||
*/
|
||||
namespace OT {
|
||||
|
||||
HB_INTERNAL void PaintColrLayers::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
c->add_layer_indices (firstLayerIndex, numLayers);
|
||||
const LayerList &paint_offset_lists = c->get_colr_table ()->get_layerList ();
|
||||
for (unsigned i = firstLayerIndex; i < firstLayerIndex + numLayers; i++)
|
||||
{
|
||||
const Paint &paint = std::addressof (paint_offset_lists) + paint_offset_lists[i];
|
||||
paint.dispatch (c);
|
||||
}
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintGlyph::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
c->add_glyph (gid);
|
||||
(this+paint).dispatch (c);
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintColrGlyph::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
const COLR *colr_table = c->get_colr_table ();
|
||||
const BaseGlyphPaintRecord* baseglyph_paintrecord = colr_table->get_base_glyph_paintrecord (gid);
|
||||
if (!baseglyph_paintrecord) return;
|
||||
c->add_glyph (gid);
|
||||
|
||||
const BaseGlyphList &baseglyph_list = colr_table->get_baseglyphList ();
|
||||
(&baseglyph_list+baseglyph_paintrecord->paint).dispatch (c);
|
||||
}
|
||||
|
||||
template <template<typename> class Var>
|
||||
HB_INTERNAL void PaintTransform<Var>::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
(this+transform).closurev1 (c);
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintTranslate::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 2;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintScale::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 2;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintScaleAroundCenter::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 4;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintScaleUniform::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 1;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintScaleUniformAroundCenter::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 3;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintRotate::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 1;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintRotateAroundCenter::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 3;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintSkew::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 2;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintSkewAroundCenter::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
c->num_var_idxes = 4;
|
||||
}
|
||||
|
||||
HB_INTERNAL void PaintComposite::closurev1 (hb_colrv1_closure_context_t* c) const
|
||||
{
|
||||
(this+src).dispatch (c);
|
||||
(this+backdrop).dispatch (c);
|
||||
}
|
||||
|
||||
} /* namespace OT */
|
||||
|
||||
|
||||
#endif /* OT_COLOR_COLR_COLRV1_CLOSURE_HH */
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright © 2016 Google, Inc.
|
||||
* Copyright © 2018 Ebrahim Byagowi
|
||||
*
|
||||
* This is part of HarfBuzz, a text shaping library.
|
||||
*
|
||||
* Permission is hereby granted, without written agreement and without
|
||||
* license or royalty fees, to use, copy, modify, and distribute this
|
||||
* software and its documentation for any purpose, provided that the
|
||||
* above copyright notice and the following two paragraphs appear in
|
||||
* all copies of this software.
|
||||
*
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*
|
||||
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
*
|
||||
* Google Author(s): Sascha Brawer
|
||||
*/
|
||||
|
||||
#ifndef OT_COLOR_CPAL_CPAL_HH
|
||||
#define OT_COLOR_CPAL_CPAL_HH
|
||||
|
||||
#include "../../../hb-open-type.hh"
|
||||
#include "../../../hb-ot-color.h"
|
||||
#include "../../../hb-ot-name.h"
|
||||
|
||||
|
||||
/*
|
||||
* CPAL -- Color Palette
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/cpal
|
||||
*/
|
||||
#define HB_OT_TAG_CPAL HB_TAG('C','P','A','L')
|
||||
|
||||
namespace OT {
|
||||
|
||||
|
||||
struct CPALV1Tail
|
||||
{
|
||||
friend struct CPAL;
|
||||
|
||||
private:
|
||||
hb_ot_color_palette_flags_t get_palette_flags (const void *base,
|
||||
unsigned int palette_index,
|
||||
unsigned int palette_count) const
|
||||
{
|
||||
if (!paletteFlagsZ) return HB_OT_COLOR_PALETTE_FLAG_DEFAULT;
|
||||
return (hb_ot_color_palette_flags_t) (uint32_t)
|
||||
(base+paletteFlagsZ).as_array (palette_count)[palette_index];
|
||||
}
|
||||
|
||||
hb_ot_name_id_t get_palette_name_id (const void *base,
|
||||
unsigned int palette_index,
|
||||
unsigned int palette_count) const
|
||||
{
|
||||
if (!paletteLabelsZ) return HB_OT_NAME_ID_INVALID;
|
||||
return (base+paletteLabelsZ).as_array (palette_count)[palette_index];
|
||||
}
|
||||
|
||||
hb_ot_name_id_t get_color_name_id (const void *base,
|
||||
unsigned int color_index,
|
||||
unsigned int color_count) const
|
||||
{
|
||||
if (!colorLabelsZ) return HB_OT_NAME_ID_INVALID;
|
||||
return (base+colorLabelsZ).as_array (color_count)[color_index];
|
||||
}
|
||||
|
||||
public:
|
||||
void collect_name_ids (const void *base,
|
||||
unsigned palette_count,
|
||||
unsigned color_count,
|
||||
const hb_map_t *color_index_map,
|
||||
hb_set_t *nameids_to_retain /* OUT */) const
|
||||
{
|
||||
if (paletteLabelsZ)
|
||||
{
|
||||
+ (base+paletteLabelsZ).as_array (palette_count)
|
||||
| hb_sink (nameids_to_retain)
|
||||
;
|
||||
}
|
||||
|
||||
if (colorLabelsZ)
|
||||
{
|
||||
const hb_array_t<const NameID> colorLabels = (base+colorLabelsZ).as_array (color_count);
|
||||
for (unsigned i = 0; i < color_count; i++)
|
||||
{
|
||||
if (!color_index_map->has (i)) continue;
|
||||
nameids_to_retain->add (colorLabels[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool serialize (hb_serialize_context_t *c,
|
||||
unsigned palette_count,
|
||||
unsigned color_count,
|
||||
const void *base,
|
||||
const hb_map_t *color_index_map) const
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
auto *out = c->allocate_size<CPALV1Tail> (static_size);
|
||||
if (unlikely (!out)) return_trace (false);
|
||||
|
||||
out->paletteFlagsZ = 0;
|
||||
if (paletteFlagsZ)
|
||||
out->paletteFlagsZ.serialize_copy (c, paletteFlagsZ, base, 0, hb_serialize_context_t::Head, palette_count);
|
||||
|
||||
out->paletteLabelsZ = 0;
|
||||
if (paletteLabelsZ)
|
||||
out->paletteLabelsZ.serialize_copy (c, paletteLabelsZ, base, 0, hb_serialize_context_t::Head, palette_count);
|
||||
|
||||
const hb_array_t<const NameID> colorLabels = (base+colorLabelsZ).as_array (color_count);
|
||||
if (colorLabelsZ)
|
||||
{
|
||||
c->push ();
|
||||
for (unsigned i = 0; i < color_count; i++)
|
||||
{
|
||||
if (!color_index_map->has (i)) continue;
|
||||
if (!c->copy<NameID> (colorLabels[i]))
|
||||
{
|
||||
c->pop_discard ();
|
||||
return_trace (false);
|
||||
}
|
||||
}
|
||||
c->add_link (out->colorLabelsZ, c->pop_pack ());
|
||||
}
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c,
|
||||
const void *base,
|
||||
unsigned int palette_count,
|
||||
unsigned int color_count) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this) &&
|
||||
(!paletteFlagsZ || (base+paletteFlagsZ).sanitize (c, palette_count)) &&
|
||||
(!paletteLabelsZ || (base+paletteLabelsZ).sanitize (c, palette_count)) &&
|
||||
(!colorLabelsZ || (base+colorLabelsZ).sanitize (c, color_count)));
|
||||
}
|
||||
|
||||
protected:
|
||||
// TODO(garretrieger): these offsets can hold nulls so we should not be using non-null offsets
|
||||
// here. Currently they are needed since UnsizedArrayOf doesn't define null_size
|
||||
NNOffset32To<UnsizedArrayOf<HBUINT32>>
|
||||
paletteFlagsZ; /* Offset from the beginning of CPAL table to
|
||||
* the Palette Type Array. Set to 0 if no array
|
||||
* is provided. */
|
||||
NNOffset32To<UnsizedArrayOf<NameID>>
|
||||
paletteLabelsZ; /* Offset from the beginning of CPAL table to
|
||||
* the palette labels array. Set to 0 if no
|
||||
* array is provided. */
|
||||
NNOffset32To<UnsizedArrayOf<NameID>>
|
||||
colorLabelsZ; /* Offset from the beginning of CPAL table to
|
||||
* the color labels array. Set to 0
|
||||
* if no array is provided. */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (12);
|
||||
};
|
||||
|
||||
typedef HBUINT32 BGRAColor;
|
||||
|
||||
struct CPAL
|
||||
{
|
||||
static constexpr hb_tag_t tableTag = HB_OT_TAG_CPAL;
|
||||
|
||||
bool has_data () const { return numPalettes; }
|
||||
|
||||
unsigned int get_size () const
|
||||
{ return min_size + numPalettes * sizeof (colorRecordIndicesZ[0]); }
|
||||
|
||||
unsigned int get_palette_count () const { return numPalettes; }
|
||||
unsigned int get_color_count () const { return numColors; }
|
||||
|
||||
hb_ot_color_palette_flags_t get_palette_flags (unsigned int palette_index) const
|
||||
{ return v1 ().get_palette_flags (this, palette_index, numPalettes); }
|
||||
|
||||
hb_ot_name_id_t get_palette_name_id (unsigned int palette_index) const
|
||||
{ return v1 ().get_palette_name_id (this, palette_index, numPalettes); }
|
||||
|
||||
hb_ot_name_id_t get_color_name_id (unsigned int color_index) const
|
||||
{ return v1 ().get_color_name_id (this, color_index, numColors); }
|
||||
|
||||
hb_array_t<const BGRAColor> get_palette_colors (unsigned int palette_index) const
|
||||
{
|
||||
if (unlikely (palette_index >= numPalettes))
|
||||
return hb_array_t<const BGRAColor> ();
|
||||
unsigned int start_index = colorRecordIndicesZ[palette_index];
|
||||
hb_array_t<const BGRAColor> all_colors ((this+colorRecordsZ).arrayZ, numColorRecords);
|
||||
return all_colors.sub_array (start_index, numColors);
|
||||
}
|
||||
unsigned int get_palette_colors (unsigned int palette_index,
|
||||
unsigned int start_offset,
|
||||
unsigned int *color_count, /* IN/OUT. May be NULL. */
|
||||
hb_color_t *colors /* OUT. May be NULL. */) const
|
||||
{
|
||||
if (unlikely (palette_index >= numPalettes))
|
||||
{
|
||||
if (color_count) *color_count = 0;
|
||||
return 0;
|
||||
}
|
||||
unsigned int start_index = colorRecordIndicesZ[palette_index];
|
||||
hb_array_t<const BGRAColor> all_colors ((this+colorRecordsZ).arrayZ, numColorRecords);
|
||||
hb_array_t<const BGRAColor> palette_colors = all_colors.sub_array (start_index,
|
||||
numColors);
|
||||
if (color_count)
|
||||
{
|
||||
+ palette_colors.sub_array (start_offset, color_count)
|
||||
| hb_sink (hb_array (colors, *color_count))
|
||||
;
|
||||
}
|
||||
return numColors;
|
||||
}
|
||||
|
||||
void collect_name_ids (const hb_map_t *color_index_map,
|
||||
hb_set_t *nameids_to_retain /* OUT */) const
|
||||
{
|
||||
if (version == 1)
|
||||
{
|
||||
hb_barrier ();
|
||||
v1 ().collect_name_ids (this, numPalettes, numColors, color_index_map, nameids_to_retain);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const CPALV1Tail& v1 () const
|
||||
{
|
||||
if (version == 0) return Null (CPALV1Tail);
|
||||
hb_barrier ();
|
||||
return StructAfter<CPALV1Tail> (*this);
|
||||
}
|
||||
|
||||
public:
|
||||
bool serialize (hb_serialize_context_t *c,
|
||||
const hb_array_t<const HBUINT16> &color_record_indices,
|
||||
const hb_array_t<const BGRAColor> &color_records,
|
||||
const hb_vector_t<unsigned>& first_color_index_for_layer,
|
||||
const hb_map_t& first_color_to_layer_index,
|
||||
const hb_set_t &retained_color_indices) const
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
|
||||
// TODO(grieger): limit total final size.
|
||||
|
||||
for (const auto idx : color_record_indices)
|
||||
{
|
||||
hb_codepoint_t layer_index = first_color_to_layer_index[idx];
|
||||
|
||||
HBUINT16 new_idx;
|
||||
new_idx = layer_index * retained_color_indices.get_population ();
|
||||
if (!c->copy<HBUINT16> (new_idx)) return_trace (false);
|
||||
}
|
||||
|
||||
c->push ();
|
||||
for (unsigned first_color_index : first_color_index_for_layer)
|
||||
{
|
||||
for (hb_codepoint_t color_index : retained_color_indices)
|
||||
{
|
||||
if (!c->copy<BGRAColor> (color_records[first_color_index + color_index]))
|
||||
{
|
||||
c->pop_discard ();
|
||||
return_trace (false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c->add_link (colorRecordsZ, c->pop_pack ());
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
if (!numPalettes) return_trace (false);
|
||||
|
||||
const hb_map_t *color_index_map = &c->plan->colr_palettes;
|
||||
if (color_index_map->is_empty ()) return_trace (false);
|
||||
|
||||
hb_set_t retained_color_indices;
|
||||
for (const auto _ : color_index_map->keys ())
|
||||
{
|
||||
if (_ == 0xFFFF) continue;
|
||||
retained_color_indices.add (_);
|
||||
}
|
||||
if (retained_color_indices.is_empty ()) return_trace (false);
|
||||
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
|
||||
|
||||
out->version = version;
|
||||
out->numColors = retained_color_indices.get_population ();
|
||||
out->numPalettes = numPalettes;
|
||||
|
||||
hb_vector_t<unsigned> first_color_index_for_layer;
|
||||
hb_map_t first_color_to_layer_index;
|
||||
|
||||
const hb_array_t<const HBUINT16> colorRecordIndices = colorRecordIndicesZ.as_array (numPalettes);
|
||||
for (const auto first_color_record_idx : colorRecordIndices)
|
||||
{
|
||||
if (first_color_to_layer_index.has (first_color_record_idx)) continue;
|
||||
|
||||
first_color_index_for_layer.push (first_color_record_idx);
|
||||
first_color_to_layer_index.set (first_color_record_idx,
|
||||
first_color_index_for_layer.length - 1);
|
||||
}
|
||||
|
||||
out->numColorRecords = first_color_index_for_layer.length
|
||||
* retained_color_indices.get_population ();
|
||||
|
||||
const hb_array_t<const BGRAColor> color_records = (this+colorRecordsZ).as_array (numColorRecords);
|
||||
if (!out->serialize (c->serializer,
|
||||
colorRecordIndices,
|
||||
color_records,
|
||||
first_color_index_for_layer,
|
||||
first_color_to_layer_index,
|
||||
retained_color_indices))
|
||||
return_trace (false);
|
||||
|
||||
if (version == 1)
|
||||
{
|
||||
hb_barrier ();
|
||||
return_trace (v1 ().serialize (c->serializer, numPalettes, numColors, this, color_index_map));
|
||||
}
|
||||
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this) &&
|
||||
hb_barrier () &&
|
||||
(this+colorRecordsZ).sanitize (c, numColorRecords) &&
|
||||
colorRecordIndicesZ.sanitize (c, numPalettes) &&
|
||||
(version == 0 || v1 ().sanitize (c, this, numPalettes, numColors)));
|
||||
}
|
||||
|
||||
protected:
|
||||
HBUINT16 version; /* Table version number */
|
||||
/* Version 0 */
|
||||
HBUINT16 numColors; /* Number of colors in each palette. */
|
||||
HBUINT16 numPalettes; /* Number of palettes in the table. */
|
||||
HBUINT16 numColorRecords; /* Total number of color records, combined for
|
||||
* all palettes. */
|
||||
NNOffset32To<UnsizedArrayOf<BGRAColor>>
|
||||
colorRecordsZ; /* Offset from the beginning of CPAL table to
|
||||
* the first ColorRecord. */
|
||||
UnsizedArrayOf<HBUINT16>
|
||||
colorRecordIndicesZ; /* Index of each palette’s first color record in
|
||||
* the combined color record array. */
|
||||
/*CPALV1Tail v1;*/
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (12, colorRecordIndicesZ);
|
||||
};
|
||||
|
||||
} /* namespace OT */
|
||||
|
||||
|
||||
#endif /* OT_COLOR_CPAL_CPAL_HH */
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
/*
|
||||
* Copyright © 2018 Ebrahim Byagowi
|
||||
* Copyright © 2020 Google, Inc.
|
||||
*
|
||||
* This is part of HarfBuzz, a text shaping library.
|
||||
*
|
||||
* Permission is hereby granted, without written agreement and without
|
||||
* license or royalty fees, to use, copy, modify, and distribute this
|
||||
* software and its documentation for any purpose, provided that the
|
||||
* above copyright notice and the following two paragraphs appear in
|
||||
* all copies of this software.
|
||||
*
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*
|
||||
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
*
|
||||
* Google Author(s): Calder Kitagawa
|
||||
*/
|
||||
|
||||
#ifndef OT_COLOR_SBIX_SBIX_HH
|
||||
#define OT_COLOR_SBIX_SBIX_HH
|
||||
|
||||
#include "../../../hb-open-type.hh"
|
||||
#include "../../../hb-paint.hh"
|
||||
|
||||
/*
|
||||
* sbix -- Standard Bitmap Graphics
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/sbix
|
||||
* https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6sbix.html
|
||||
*/
|
||||
#define HB_OT_TAG_sbix HB_TAG('s','b','i','x')
|
||||
|
||||
|
||||
namespace OT {
|
||||
|
||||
|
||||
struct SBIXGlyph
|
||||
{
|
||||
SBIXGlyph* copy (hb_serialize_context_t *c, unsigned int data_length) const
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
SBIXGlyph* new_glyph = c->start_embed<SBIXGlyph> ();
|
||||
if (unlikely (!c->extend_min (new_glyph))) return_trace (nullptr);
|
||||
|
||||
new_glyph->xOffset = xOffset;
|
||||
new_glyph->yOffset = yOffset;
|
||||
new_glyph->graphicType = graphicType;
|
||||
data.copy (c, data_length);
|
||||
return_trace (new_glyph);
|
||||
}
|
||||
|
||||
HBINT16 xOffset; /* The horizontal (x-axis) offset from the left
|
||||
* edge of the graphic to the glyph’s origin.
|
||||
* That is, the x-coordinate of the point on the
|
||||
* baseline at the left edge of the glyph. */
|
||||
HBINT16 yOffset; /* The vertical (y-axis) offset from the bottom
|
||||
* edge of the graphic to the glyph’s origin.
|
||||
* That is, the y-coordinate of the point on the
|
||||
* baseline at the left edge of the glyph. */
|
||||
Tag graphicType; /* Indicates the format of the embedded graphic
|
||||
* data: one of 'jpg ', 'png ' or 'tiff', or the
|
||||
* special format 'dupe'. */
|
||||
UnsizedArrayOf<HBUINT8>
|
||||
data; /* The actual embedded graphic data. The total
|
||||
* length is inferred from sequential entries in
|
||||
* the glyphDataOffsets array and the fixed size
|
||||
* (8 bytes) of the preceding fields. */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (8, data);
|
||||
};
|
||||
|
||||
struct SBIXStrike
|
||||
{
|
||||
static unsigned int get_size (unsigned num_glyphs)
|
||||
{ return min_size + num_glyphs * HBUINT32::static_size; }
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this) &&
|
||||
imageOffsetsZ.sanitize_shallow (c, c->get_num_glyphs () + 1));
|
||||
}
|
||||
|
||||
hb_blob_t *get_glyph_blob (unsigned int glyph_id,
|
||||
hb_blob_t *sbix_blob,
|
||||
hb_tag_t file_type,
|
||||
int *x_offset,
|
||||
int *y_offset,
|
||||
unsigned int num_glyphs,
|
||||
unsigned int *strike_ppem) const
|
||||
{
|
||||
if (unlikely (!ppem)) return hb_blob_get_empty (); /* To get Null() object out of the way. */
|
||||
|
||||
unsigned int retry_count = 8;
|
||||
unsigned int sbix_len = sbix_blob->length;
|
||||
unsigned int strike_offset = (const char *) this - (const char *) sbix_blob->data;
|
||||
assert (strike_offset < sbix_len);
|
||||
|
||||
retry:
|
||||
if (unlikely (glyph_id >= num_glyphs ||
|
||||
imageOffsetsZ[glyph_id + 1] <= imageOffsetsZ[glyph_id] ||
|
||||
imageOffsetsZ[glyph_id + 1] - imageOffsetsZ[glyph_id] <= SBIXGlyph::min_size ||
|
||||
(unsigned int) imageOffsetsZ[glyph_id + 1] > sbix_len - strike_offset))
|
||||
return hb_blob_get_empty ();
|
||||
|
||||
unsigned int glyph_offset = strike_offset + (unsigned int) imageOffsetsZ[glyph_id] + SBIXGlyph::min_size;
|
||||
unsigned int glyph_length = imageOffsetsZ[glyph_id + 1] - imageOffsetsZ[glyph_id] - SBIXGlyph::min_size;
|
||||
|
||||
const SBIXGlyph *glyph = &(this+imageOffsetsZ[glyph_id]);
|
||||
|
||||
if (glyph->graphicType == HB_TAG ('d','u','p','e'))
|
||||
{
|
||||
if (glyph_length >= 2)
|
||||
{
|
||||
glyph_id = *((HBUINT16 *) &glyph->data);
|
||||
if (retry_count--)
|
||||
goto retry;
|
||||
}
|
||||
return hb_blob_get_empty ();
|
||||
}
|
||||
|
||||
if (unlikely (file_type != glyph->graphicType))
|
||||
return hb_blob_get_empty ();
|
||||
|
||||
if (strike_ppem) *strike_ppem = ppem;
|
||||
if (x_offset) *x_offset = glyph->xOffset;
|
||||
if (y_offset) *y_offset = glyph->yOffset;
|
||||
return hb_blob_create_sub_blob (sbix_blob, glyph_offset, glyph_length);
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c, unsigned int available_len) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
unsigned int num_output_glyphs = c->plan->num_output_glyphs ();
|
||||
|
||||
auto* out = c->serializer->start_embed<SBIXStrike> ();
|
||||
auto snap = c->serializer->snapshot ();
|
||||
if (unlikely (!c->serializer->extend (out, num_output_glyphs + 1))) return_trace (false);
|
||||
out->ppem = ppem;
|
||||
out->resolution = resolution;
|
||||
HBUINT32 head;
|
||||
head = get_size (num_output_glyphs + 1);
|
||||
|
||||
bool has_glyphs = false;
|
||||
for (unsigned new_gid = 0; new_gid < num_output_glyphs; new_gid++)
|
||||
{
|
||||
hb_codepoint_t old_gid;
|
||||
if (!c->plan->old_gid_for_new_gid (new_gid, &old_gid) ||
|
||||
unlikely (imageOffsetsZ[old_gid].is_null () ||
|
||||
imageOffsetsZ[old_gid + 1].is_null () ||
|
||||
imageOffsetsZ[old_gid + 1] <= imageOffsetsZ[old_gid] ||
|
||||
imageOffsetsZ[old_gid + 1] - imageOffsetsZ[old_gid] <= SBIXGlyph::min_size) ||
|
||||
(unsigned int) imageOffsetsZ[old_gid + 1] > available_len)
|
||||
{
|
||||
out->imageOffsetsZ[new_gid] = head;
|
||||
continue;
|
||||
}
|
||||
has_glyphs = true;
|
||||
unsigned int delta = imageOffsetsZ[old_gid + 1] - imageOffsetsZ[old_gid];
|
||||
unsigned int glyph_data_length = delta - SBIXGlyph::min_size;
|
||||
if (!(this+imageOffsetsZ[old_gid]).copy (c->serializer, glyph_data_length))
|
||||
return_trace (false);
|
||||
out->imageOffsetsZ[new_gid] = head;
|
||||
head += delta;
|
||||
}
|
||||
if (has_glyphs)
|
||||
out->imageOffsetsZ[num_output_glyphs] = head;
|
||||
else
|
||||
c->serializer->revert (snap);
|
||||
return_trace (has_glyphs);
|
||||
}
|
||||
|
||||
public:
|
||||
HBUINT16 ppem; /* The PPEM size for which this strike was designed. */
|
||||
HBUINT16 resolution; /* The device pixel density (in PPI) for which this
|
||||
* strike was designed. (E.g., 96 PPI, 192 PPI.) */
|
||||
protected:
|
||||
UnsizedArrayOf<Offset32To<SBIXGlyph>>
|
||||
imageOffsetsZ; /* Offset from the beginning of the strike data header
|
||||
* to bitmap data for an individual glyph ID. */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (4, imageOffsetsZ);
|
||||
};
|
||||
|
||||
struct sbix
|
||||
{
|
||||
static constexpr hb_tag_t tableTag = HB_OT_TAG_sbix;
|
||||
|
||||
bool has_data () const { return version; }
|
||||
|
||||
const SBIXStrike &get_strike (unsigned int i) const { return this+strikes[i]; }
|
||||
|
||||
struct accelerator_t
|
||||
{
|
||||
accelerator_t (hb_face_t *face)
|
||||
{
|
||||
table = hb_sanitize_context_t ().reference_table<sbix> (face);
|
||||
num_glyphs = face->get_num_glyphs ();
|
||||
}
|
||||
~accelerator_t () { table.destroy (); }
|
||||
|
||||
bool has_data () const { return table->has_data (); }
|
||||
|
||||
bool get_extents (hb_font_t *font,
|
||||
hb_codepoint_t glyph,
|
||||
hb_glyph_extents_t *extents,
|
||||
bool scale = true) const
|
||||
{
|
||||
/* We only support PNG right now, and following function checks type. */
|
||||
return get_png_extents (font, glyph, extents, scale);
|
||||
}
|
||||
|
||||
hb_blob_t *reference_png (hb_font_t *font,
|
||||
hb_codepoint_t glyph_id,
|
||||
int *x_offset,
|
||||
int *y_offset,
|
||||
unsigned int *available_ppem) const
|
||||
{
|
||||
return choose_strike (font).get_glyph_blob (glyph_id, table.get_blob (),
|
||||
HB_TAG ('p','n','g',' '),
|
||||
x_offset, y_offset,
|
||||
num_glyphs, available_ppem);
|
||||
}
|
||||
|
||||
bool paint_glyph (hb_font_t *font, hb_codepoint_t glyph, hb_paint_funcs_t *funcs, void *data) const
|
||||
{
|
||||
if (!has_data ())
|
||||
return false;
|
||||
|
||||
int x_offset = 0, y_offset = 0;
|
||||
unsigned int strike_ppem = 0;
|
||||
hb_glyph_extents_t extents;
|
||||
hb_glyph_extents_t pixel_extents;
|
||||
|
||||
if (!font->get_glyph_extents (glyph, &extents, false))
|
||||
return false;
|
||||
|
||||
if (unlikely (!get_extents (font, glyph, &pixel_extents, false)))
|
||||
return false;
|
||||
|
||||
hb_blob_t *blob = reference_png (font, glyph, &x_offset, &y_offset, &strike_ppem);
|
||||
if (hb_blob_is_immutable (blob))
|
||||
return false;
|
||||
|
||||
bool ret = funcs->image (data,
|
||||
blob,
|
||||
pixel_extents.width, -pixel_extents.height,
|
||||
HB_PAINT_IMAGE_FORMAT_PNG,
|
||||
0.f,
|
||||
&extents);
|
||||
|
||||
hb_blob_destroy (blob);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
const SBIXStrike &choose_strike (hb_font_t *font) const
|
||||
{
|
||||
unsigned count = table->strikes.len;
|
||||
if (unlikely (!count))
|
||||
return Null (SBIXStrike);
|
||||
|
||||
unsigned int requested_ppem = hb_max (font->x_ppem, font->y_ppem);
|
||||
if (!requested_ppem)
|
||||
requested_ppem = 1<<30; /* Choose largest strike. */
|
||||
/* TODO Add DPI sensitivity as well? */
|
||||
unsigned int best_i = 0;
|
||||
unsigned int best_ppem = table->get_strike (0).ppem;
|
||||
|
||||
for (unsigned int i = 1; i < count; i++)
|
||||
{
|
||||
unsigned int ppem = (table->get_strike (i)).ppem;
|
||||
if ((requested_ppem <= ppem && ppem < best_ppem) ||
|
||||
(requested_ppem > best_ppem && ppem > best_ppem))
|
||||
{
|
||||
best_i = i;
|
||||
best_ppem = ppem;
|
||||
}
|
||||
}
|
||||
|
||||
return table->get_strike (best_i);
|
||||
}
|
||||
|
||||
struct PNGHeader
|
||||
{
|
||||
HBUINT8 signature[8];
|
||||
struct
|
||||
{
|
||||
struct
|
||||
{
|
||||
HBUINT32 length;
|
||||
Tag type;
|
||||
} header;
|
||||
HBUINT32 width;
|
||||
HBUINT32 height;
|
||||
HBUINT8 bitDepth;
|
||||
HBUINT8 colorType;
|
||||
HBUINT8 compressionMethod;
|
||||
HBUINT8 filterMethod;
|
||||
HBUINT8 interlaceMethod;
|
||||
} IHDR;
|
||||
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (29);
|
||||
};
|
||||
|
||||
bool get_png_extents (hb_font_t *font,
|
||||
hb_codepoint_t glyph,
|
||||
hb_glyph_extents_t *extents,
|
||||
bool scale = true) const
|
||||
{
|
||||
/* Following code is safe to call even without data.
|
||||
* But faster to short-circuit. */
|
||||
if (!has_data ())
|
||||
return false;
|
||||
|
||||
int x_offset = 0, y_offset = 0;
|
||||
unsigned int strike_ppem = 0;
|
||||
hb_blob_t *blob = reference_png (font, glyph, &x_offset, &y_offset, &strike_ppem);
|
||||
|
||||
const PNGHeader &png = *blob->as<PNGHeader>();
|
||||
|
||||
if (png.IHDR.height >= 65536 || png.IHDR.width >= 65536)
|
||||
{
|
||||
hb_blob_destroy (blob);
|
||||
return false;
|
||||
}
|
||||
|
||||
extents->x_bearing = x_offset;
|
||||
extents->y_bearing = png.IHDR.height + y_offset;
|
||||
extents->width = png.IHDR.width;
|
||||
extents->height = -1 * png.IHDR.height;
|
||||
|
||||
/* Convert to font units. */
|
||||
if (strike_ppem && scale)
|
||||
{
|
||||
float scale = font->face->get_upem () / (float) strike_ppem;
|
||||
extents->x_bearing = roundf (extents->x_bearing * scale);
|
||||
extents->y_bearing = roundf (extents->y_bearing * scale);
|
||||
extents->width = roundf (extents->width * scale);
|
||||
extents->height = roundf (extents->height * scale);
|
||||
}
|
||||
|
||||
if (scale)
|
||||
font->scale_glyph_extents (extents);
|
||||
|
||||
hb_blob_destroy (blob);
|
||||
|
||||
return strike_ppem;
|
||||
}
|
||||
|
||||
private:
|
||||
hb_blob_ptr_t<sbix> table;
|
||||
|
||||
unsigned int num_glyphs;
|
||||
};
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (likely (c->check_struct (this) &&
|
||||
hb_barrier () &&
|
||||
version >= 1 &&
|
||||
strikes.sanitize (c, this)));
|
||||
}
|
||||
|
||||
bool
|
||||
add_strike (hb_subset_context_t *c, unsigned i) const
|
||||
{
|
||||
if (strikes[i].is_null () || c->source_blob->length < (unsigned) strikes[i])
|
||||
return false;
|
||||
|
||||
return (this+strikes[i]).subset (c, c->source_blob->length - (unsigned) strikes[i]);
|
||||
}
|
||||
|
||||
bool serialize_strike_offsets (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
|
||||
auto *out = c->serializer->start_embed<Array32OfOffset32To<SBIXStrike>> ();
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
|
||||
hb_vector_t<Offset32To<SBIXStrike>*> new_strikes;
|
||||
hb_vector_t<hb_serialize_context_t::objidx_t> objidxs;
|
||||
for (int i = strikes.len - 1; i >= 0; --i)
|
||||
{
|
||||
auto* o = out->serialize_append (c->serializer);
|
||||
if (unlikely (!o)) return_trace (false);
|
||||
*o = 0;
|
||||
auto snap = c->serializer->snapshot ();
|
||||
c->serializer->push ();
|
||||
bool ret = add_strike (c, i);
|
||||
if (!ret)
|
||||
{
|
||||
c->serializer->pop_discard ();
|
||||
out->pop ();
|
||||
c->serializer->revert (snap);
|
||||
}
|
||||
else
|
||||
{
|
||||
objidxs.push (c->serializer->pop_pack ());
|
||||
new_strikes.push (o);
|
||||
}
|
||||
}
|
||||
for (unsigned int i = 0; i < new_strikes.length; ++i)
|
||||
c->serializer->add_link (*new_strikes[i], objidxs[new_strikes.length - 1 - i]);
|
||||
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t* c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
|
||||
if (unlikely (!c->serializer->embed (this->version))) return_trace (false);
|
||||
if (unlikely (!c->serializer->embed (this->flags))) return_trace (false);
|
||||
|
||||
return_trace (serialize_strike_offsets (c));
|
||||
}
|
||||
|
||||
protected:
|
||||
HBUINT16 version; /* Table version number — set to 1 */
|
||||
HBUINT16 flags; /* Bit 0: Set to 1. Bit 1: Draw outlines.
|
||||
* Bits 2 to 15: reserved (set to 0). */
|
||||
Array32OfOffset32To<SBIXStrike>
|
||||
strikes; /* Offsets from the beginning of the 'sbix'
|
||||
* table to data for each individual bitmap strike. */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (8, strikes);
|
||||
};
|
||||
|
||||
struct sbix_accelerator_t : sbix::accelerator_t {
|
||||
sbix_accelerator_t (hb_face_t *face) : sbix::accelerator_t (face) {}
|
||||
};
|
||||
|
||||
|
||||
} /* namespace OT */
|
||||
|
||||
#endif /* OT_COLOR_SBIX_SBIX_HH */
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright © 2018 Ebrahim Byagowi
|
||||
*
|
||||
* This is part of HarfBuzz, a text shaping library.
|
||||
*
|
||||
* Permission is hereby granted, without written agreement and without
|
||||
* license or royalty fees, to use, copy, modify, and distribute this
|
||||
* software and its documentation for any purpose, provided that the
|
||||
* above copyright notice and the following two paragraphs appear in
|
||||
* all copies of this software.
|
||||
*
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*
|
||||
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
*/
|
||||
|
||||
#ifndef OT_COLOR_SVG_SVG_HH
|
||||
#define OT_COLOR_SVG_SVG_HH
|
||||
|
||||
#include "../../../hb-open-type.hh"
|
||||
#include "../../../hb-blob.hh"
|
||||
#include "../../../hb-paint.hh"
|
||||
|
||||
/*
|
||||
* SVG -- SVG (Scalable Vector Graphics)
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/svg
|
||||
*/
|
||||
|
||||
#define HB_OT_TAG_SVG HB_TAG('S','V','G',' ')
|
||||
|
||||
|
||||
namespace OT {
|
||||
|
||||
|
||||
struct SVGDocumentIndexEntry
|
||||
{
|
||||
int cmp (hb_codepoint_t g) const
|
||||
{ return g < startGlyphID ? -1 : g > endGlyphID ? 1 : 0; }
|
||||
|
||||
hb_blob_t *reference_blob (hb_blob_t *svg_blob, unsigned int index_offset) const
|
||||
{
|
||||
return hb_blob_create_sub_blob (svg_blob,
|
||||
index_offset + (unsigned int) svgDoc,
|
||||
svgDocLength);
|
||||
}
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c, const void *base) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this) &&
|
||||
hb_barrier () &&
|
||||
svgDoc.sanitize (c, base, svgDocLength));
|
||||
}
|
||||
|
||||
protected:
|
||||
HBUINT16 startGlyphID; /* The first glyph ID in the range described by
|
||||
* this index entry. */
|
||||
HBUINT16 endGlyphID; /* The last glyph ID in the range described by
|
||||
* this index entry. Must be >= startGlyphID. */
|
||||
NNOffset32To<UnsizedArrayOf<HBUINT8>>
|
||||
svgDoc; /* Offset from the beginning of the SVG Document Index
|
||||
* to an SVG document. Must be non-zero. */
|
||||
HBUINT32 svgDocLength; /* Length of the SVG document.
|
||||
* Must be non-zero. */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (12);
|
||||
};
|
||||
|
||||
struct SVG
|
||||
{
|
||||
static constexpr hb_tag_t tableTag = HB_OT_TAG_SVG;
|
||||
|
||||
bool has_data () const { return svgDocEntries; }
|
||||
|
||||
struct accelerator_t
|
||||
{
|
||||
accelerator_t (hb_face_t *face)
|
||||
{ table = hb_sanitize_context_t ().reference_table<SVG> (face); }
|
||||
~accelerator_t () { table.destroy (); }
|
||||
|
||||
hb_blob_t *reference_blob_for_glyph (hb_codepoint_t glyph_id) const
|
||||
{
|
||||
return table->get_glyph_entry (glyph_id).reference_blob (table.get_blob (),
|
||||
table->svgDocEntries);
|
||||
}
|
||||
|
||||
bool has_data () const { return table->has_data (); }
|
||||
|
||||
bool paint_glyph (hb_font_t *font HB_UNUSED, hb_codepoint_t glyph, hb_paint_funcs_t *funcs, void *data) const
|
||||
{
|
||||
if (!has_data ())
|
||||
return false;
|
||||
|
||||
hb_blob_t *blob = reference_blob_for_glyph (glyph);
|
||||
|
||||
if (blob == hb_blob_get_empty ())
|
||||
return false;
|
||||
|
||||
bool ret = funcs->image (data,
|
||||
blob,
|
||||
0, 0,
|
||||
HB_PAINT_IMAGE_FORMAT_SVG,
|
||||
0.f,
|
||||
nullptr);
|
||||
|
||||
hb_blob_destroy (blob);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private:
|
||||
hb_blob_ptr_t<SVG> table;
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (sizeof (hb_blob_ptr_t<SVG>));
|
||||
};
|
||||
|
||||
const SVGDocumentIndexEntry &get_glyph_entry (hb_codepoint_t glyph_id) const
|
||||
{ return (this+svgDocEntries).bsearch (glyph_id); }
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (likely (c->check_struct (this) &&
|
||||
(this+svgDocEntries).sanitize_shallow (c)));
|
||||
}
|
||||
|
||||
protected:
|
||||
HBUINT16 version; /* Table version (starting at 0). */
|
||||
Offset32To<SortedArray16Of<SVGDocumentIndexEntry>>
|
||||
svgDocEntries; /* Offset (relative to the start of the SVG table) to the
|
||||
* SVG Documents Index. Must be non-zero. */
|
||||
/* Array of SVG Document Index Entries. */
|
||||
HBUINT32 reserved; /* Set to 0. */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (10);
|
||||
};
|
||||
|
||||
struct SVG_accelerator_t : SVG::accelerator_t {
|
||||
SVG_accelerator_t (hb_face_t *face) : SVG::accelerator_t (face) {}
|
||||
};
|
||||
|
||||
} /* namespace OT */
|
||||
|
||||
|
||||
#endif /* OT_COLOR_SVG_SVG_HH */
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
/*
|
||||
* Copyright © 2007,2008,2009 Red Hat, Inc.
|
||||
* Copyright © 2010,2012 Google, Inc.
|
||||
*
|
||||
* This is part of HarfBuzz, a text shaping library.
|
||||
*
|
||||
* Permission is hereby granted, without written agreement and without
|
||||
* license or royalty fees, to use, copy, modify, and distribute this
|
||||
* software and its documentation for any purpose, provided that the
|
||||
* above copyright notice and the following two paragraphs appear in
|
||||
* all copies of this software.
|
||||
*
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*
|
||||
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
*
|
||||
* Red Hat Author(s): Behdad Esfahbod
|
||||
* Google Author(s): Behdad Esfahbod, Garret Rieger
|
||||
*/
|
||||
|
||||
#ifndef OT_LAYOUT_COMMON_COVERAGE_HH
|
||||
#define OT_LAYOUT_COMMON_COVERAGE_HH
|
||||
|
||||
#include "../types.hh"
|
||||
#include "CoverageFormat1.hh"
|
||||
#include "CoverageFormat2.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace Common {
|
||||
|
||||
template<typename Iterator>
|
||||
static inline void Coverage_serialize (hb_serialize_context_t *c,
|
||||
Iterator it);
|
||||
|
||||
struct Coverage
|
||||
{
|
||||
|
||||
protected:
|
||||
union {
|
||||
HBUINT16 format; /* Format identifier */
|
||||
CoverageFormat1_3<SmallTypes> format1;
|
||||
CoverageFormat2_4<SmallTypes> format2;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
CoverageFormat1_3<MediumTypes>format3;
|
||||
CoverageFormat2_4<MediumTypes>format4;
|
||||
#endif
|
||||
} u;
|
||||
public:
|
||||
DEFINE_SIZE_UNION (2, format);
|
||||
|
||||
#ifndef HB_OPTIMIZE_SIZE
|
||||
HB_ALWAYS_INLINE
|
||||
#endif
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
if (!u.format.sanitize (c)) return_trace (false);
|
||||
hb_barrier ();
|
||||
switch (u.format)
|
||||
{
|
||||
case 1: return_trace (u.format1.sanitize (c));
|
||||
case 2: return_trace (u.format2.sanitize (c));
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return_trace (u.format3.sanitize (c));
|
||||
case 4: return_trace (u.format4.sanitize (c));
|
||||
#endif
|
||||
default:return_trace (true);
|
||||
}
|
||||
}
|
||||
|
||||
/* Has interface. */
|
||||
unsigned operator [] (hb_codepoint_t k) const { return get (k); }
|
||||
bool has (hb_codepoint_t k) const { return (*this)[k] != NOT_COVERED; }
|
||||
/* Predicate. */
|
||||
bool operator () (hb_codepoint_t k) const { return has (k); }
|
||||
|
||||
unsigned int get (hb_codepoint_t k) const { return get_coverage (k); }
|
||||
unsigned int get_coverage (hb_codepoint_t glyph_id) const
|
||||
{
|
||||
switch (u.format) {
|
||||
case 1: return u.format1.get_coverage (glyph_id);
|
||||
case 2: return u.format2.get_coverage (glyph_id);
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3.get_coverage (glyph_id);
|
||||
case 4: return u.format4.get_coverage (glyph_id);
|
||||
#endif
|
||||
default:return NOT_COVERED;
|
||||
}
|
||||
}
|
||||
unsigned int get_coverage (hb_codepoint_t glyph_id,
|
||||
hb_ot_lookup_cache_t *cache) const
|
||||
{
|
||||
unsigned coverage;
|
||||
if (cache && cache->get (glyph_id, &coverage)) return coverage;
|
||||
coverage = get_coverage (glyph_id);
|
||||
if (cache) cache->set (glyph_id, coverage);
|
||||
return coverage;
|
||||
}
|
||||
|
||||
unsigned get_population () const
|
||||
{
|
||||
switch (u.format) {
|
||||
case 1: return u.format1.get_population ();
|
||||
case 2: return u.format2.get_population ();
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3.get_population ();
|
||||
case 4: return u.format4.get_population ();
|
||||
#endif
|
||||
default:return NOT_COVERED;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Iterator,
|
||||
hb_requires (hb_is_sorted_source_of (Iterator, hb_codepoint_t))>
|
||||
bool serialize (hb_serialize_context_t *c, Iterator glyphs)
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
if (unlikely (!c->extend_min (this))) return_trace (false);
|
||||
|
||||
unsigned count = hb_len (glyphs);
|
||||
unsigned num_ranges = 0;
|
||||
hb_codepoint_t last = (hb_codepoint_t) -2;
|
||||
hb_codepoint_t max = 0;
|
||||
bool unsorted = false;
|
||||
for (auto g: glyphs)
|
||||
{
|
||||
if (last != (hb_codepoint_t) -2 && g < last)
|
||||
unsorted = true;
|
||||
if (last + 1 != g)
|
||||
num_ranges++;
|
||||
last = g;
|
||||
if (g > max) max = g;
|
||||
}
|
||||
u.format = !unsorted && count <= num_ranges * 3 ? 1 : 2;
|
||||
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
if (max > 0xFFFFu)
|
||||
u.format += 2;
|
||||
if (unlikely (max > 0xFFFFFFu))
|
||||
#else
|
||||
if (unlikely (max > 0xFFFFu))
|
||||
#endif
|
||||
{
|
||||
c->check_success (false, HB_SERIALIZE_ERROR_INT_OVERFLOW);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
switch (u.format)
|
||||
{
|
||||
case 1: return_trace (u.format1.serialize (c, glyphs));
|
||||
case 2: return_trace (u.format2.serialize (c, glyphs));
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return_trace (u.format3.serialize (c, glyphs));
|
||||
case 4: return_trace (u.format4.serialize (c, glyphs));
|
||||
#endif
|
||||
default:return_trace (false);
|
||||
}
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
auto it =
|
||||
+ iter ()
|
||||
| hb_take (c->plan->source->get_num_glyphs ())
|
||||
| hb_map_retains_sorting (c->plan->glyph_map_gsub)
|
||||
| hb_filter ([] (hb_codepoint_t glyph) { return glyph != HB_MAP_VALUE_INVALID; })
|
||||
;
|
||||
|
||||
// Cache the iterator result as it will be iterated multiple times
|
||||
// by the serialize code below.
|
||||
hb_sorted_vector_t<hb_codepoint_t> glyphs (it);
|
||||
Coverage_serialize (c->serializer, glyphs.iter ());
|
||||
return_trace (bool (glyphs));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
switch (u.format)
|
||||
{
|
||||
case 1: return u.format1.intersects (glyphs);
|
||||
case 2: return u.format2.intersects (glyphs);
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3.intersects (glyphs);
|
||||
case 4: return u.format4.intersects (glyphs);
|
||||
#endif
|
||||
default:return false;
|
||||
}
|
||||
}
|
||||
bool intersects_coverage (const hb_set_t *glyphs, unsigned int index) const
|
||||
{
|
||||
switch (u.format)
|
||||
{
|
||||
case 1: return u.format1.intersects_coverage (glyphs, index);
|
||||
case 2: return u.format2.intersects_coverage (glyphs, index);
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3.intersects_coverage (glyphs, index);
|
||||
case 4: return u.format4.intersects_coverage (glyphs, index);
|
||||
#endif
|
||||
default:return false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned cost () const
|
||||
{
|
||||
switch (u.format) {
|
||||
case 1: hb_barrier (); return u.format1.cost ();
|
||||
case 2: hb_barrier (); return u.format2.cost ();
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: hb_barrier (); return u.format3.cost ();
|
||||
case 4: hb_barrier (); return u.format4.cost ();
|
||||
#endif
|
||||
default:return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/* Might return false if array looks unsorted.
|
||||
* Used for faster rejection of corrupt data. */
|
||||
template <typename set_t>
|
||||
bool collect_coverage (set_t *glyphs) const
|
||||
{
|
||||
switch (u.format)
|
||||
{
|
||||
case 1: return u.format1.collect_coverage (glyphs);
|
||||
case 2: return u.format2.collect_coverage (glyphs);
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3.collect_coverage (glyphs);
|
||||
case 4: return u.format4.collect_coverage (glyphs);
|
||||
#endif
|
||||
default:return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename IterableOut,
|
||||
hb_requires (hb_is_sink_of (IterableOut, hb_codepoint_t))>
|
||||
void intersect_set (const hb_set_t &glyphs, IterableOut&& intersect_glyphs) const
|
||||
{
|
||||
switch (u.format)
|
||||
{
|
||||
case 1: return u.format1.intersect_set (glyphs, intersect_glyphs);
|
||||
case 2: return u.format2.intersect_set (glyphs, intersect_glyphs);
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3.intersect_set (glyphs, intersect_glyphs);
|
||||
case 4: return u.format4.intersect_set (glyphs, intersect_glyphs);
|
||||
#endif
|
||||
default:return ;
|
||||
}
|
||||
}
|
||||
|
||||
struct iter_t : hb_iter_with_fallback_t<iter_t, hb_codepoint_t>
|
||||
{
|
||||
static constexpr bool is_sorted_iterator = true;
|
||||
iter_t (const Coverage &c_ = Null (Coverage))
|
||||
{
|
||||
hb_memset (this, 0, sizeof (*this));
|
||||
format = c_.u.format;
|
||||
switch (format)
|
||||
{
|
||||
case 1: u.format1.init (c_.u.format1); return;
|
||||
case 2: u.format2.init (c_.u.format2); return;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: u.format3.init (c_.u.format3); return;
|
||||
case 4: u.format4.init (c_.u.format4); return;
|
||||
#endif
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
bool __more__ () const
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case 1: return u.format1.__more__ ();
|
||||
case 2: return u.format2.__more__ ();
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3.__more__ ();
|
||||
case 4: return u.format4.__more__ ();
|
||||
#endif
|
||||
default:return false;
|
||||
}
|
||||
}
|
||||
void __next__ ()
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case 1: u.format1.__next__ (); break;
|
||||
case 2: u.format2.__next__ (); break;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: u.format3.__next__ (); break;
|
||||
case 4: u.format4.__next__ (); break;
|
||||
#endif
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
typedef hb_codepoint_t __item_t__;
|
||||
__item_t__ __item__ () const { return get_glyph (); }
|
||||
|
||||
hb_codepoint_t get_glyph () const
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case 1: return u.format1.get_glyph ();
|
||||
case 2: return u.format2.get_glyph ();
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3.get_glyph ();
|
||||
case 4: return u.format4.get_glyph ();
|
||||
#endif
|
||||
default:return 0;
|
||||
}
|
||||
}
|
||||
bool operator != (const iter_t& o) const
|
||||
{
|
||||
if (unlikely (format != o.format)) return true;
|
||||
switch (format)
|
||||
{
|
||||
case 1: return u.format1 != o.u.format1;
|
||||
case 2: return u.format2 != o.u.format2;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return u.format3 != o.u.format3;
|
||||
case 4: return u.format4 != o.u.format4;
|
||||
#endif
|
||||
default:return false;
|
||||
}
|
||||
}
|
||||
iter_t __end__ () const
|
||||
{
|
||||
iter_t it = {};
|
||||
it.format = format;
|
||||
switch (format)
|
||||
{
|
||||
case 1: it.u.format1 = u.format1.__end__ (); break;
|
||||
case 2: it.u.format2 = u.format2.__end__ (); break;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: it.u.format3 = u.format3.__end__ (); break;
|
||||
case 4: it.u.format4 = u.format4.__end__ (); break;
|
||||
#endif
|
||||
default: break;
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
private:
|
||||
unsigned int format;
|
||||
union {
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
CoverageFormat2_4<MediumTypes>::iter_t format4; /* Put this one first since it's larger; helps shut up compiler. */
|
||||
CoverageFormat1_3<MediumTypes>::iter_t format3;
|
||||
#endif
|
||||
CoverageFormat2_4<SmallTypes>::iter_t format2; /* Put this one first since it's larger; helps shut up compiler. */
|
||||
CoverageFormat1_3<SmallTypes>::iter_t format1;
|
||||
} u;
|
||||
};
|
||||
iter_t iter () const { return iter_t (*this); }
|
||||
};
|
||||
|
||||
template<typename Iterator>
|
||||
static inline void
|
||||
Coverage_serialize (hb_serialize_context_t *c,
|
||||
Iterator it)
|
||||
{ c->start_embed<Coverage> ()->serialize (c, it); }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // #ifndef OT_LAYOUT_COMMON_COVERAGE_HH
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright © 2007,2008,2009 Red Hat, Inc.
|
||||
* Copyright © 2010,2012 Google, Inc.
|
||||
*
|
||||
* This is part of HarfBuzz, a text shaping library.
|
||||
*
|
||||
* Permission is hereby granted, without written agreement and without
|
||||
* license or royalty fees, to use, copy, modify, and distribute this
|
||||
* software and its documentation for any purpose, provided that the
|
||||
* above copyright notice and the following two paragraphs appear in
|
||||
* all copies of this software.
|
||||
*
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*
|
||||
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
*
|
||||
* Red Hat Author(s): Behdad Esfahbod
|
||||
* Google Author(s): Behdad Esfahbod, Garret Rieger
|
||||
*/
|
||||
|
||||
|
||||
#ifndef OT_LAYOUT_COMMON_COVERAGEFORMAT1_HH
|
||||
#define OT_LAYOUT_COMMON_COVERAGEFORMAT1_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace Common {
|
||||
|
||||
#define NOT_COVERED ((unsigned int) -1)
|
||||
|
||||
template <typename Types>
|
||||
struct CoverageFormat1_3
|
||||
{
|
||||
friend struct Coverage;
|
||||
|
||||
protected:
|
||||
HBUINT16 coverageFormat; /* Format identifier--format = 1 */
|
||||
SortedArray16Of<typename Types::HBGlyphID>
|
||||
glyphArray; /* Array of GlyphIDs--in numerical order */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (4, glyphArray);
|
||||
|
||||
private:
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (glyphArray.sanitize (c));
|
||||
}
|
||||
|
||||
unsigned int get_coverage (hb_codepoint_t glyph_id) const
|
||||
{
|
||||
unsigned int i;
|
||||
glyphArray.bfind (glyph_id, &i, HB_NOT_FOUND_STORE, NOT_COVERED);
|
||||
return i;
|
||||
}
|
||||
|
||||
unsigned get_population () const
|
||||
{
|
||||
return glyphArray.len;
|
||||
}
|
||||
|
||||
template <typename Iterator,
|
||||
hb_requires (hb_is_sorted_source_of (Iterator, hb_codepoint_t))>
|
||||
bool serialize (hb_serialize_context_t *c, Iterator glyphs)
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
return_trace (glyphArray.serialize (c, glyphs));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
if (glyphArray.len > glyphs->get_population () * hb_bit_storage ((unsigned) glyphArray.len))
|
||||
{
|
||||
for (auto g : *glyphs)
|
||||
if (get_coverage (g) != NOT_COVERED)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& g : glyphArray.as_array ())
|
||||
if (glyphs->has (g))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
bool intersects_coverage (const hb_set_t *glyphs, unsigned int index) const
|
||||
{ return glyphs->has (glyphArray[index]); }
|
||||
|
||||
template <typename IterableOut,
|
||||
hb_requires (hb_is_sink_of (IterableOut, hb_codepoint_t))>
|
||||
void intersect_set (const hb_set_t &glyphs, IterableOut&& intersect_glyphs) const
|
||||
{
|
||||
unsigned count = glyphArray.len;
|
||||
for (unsigned i = 0; i < count; i++)
|
||||
if (glyphs.has (glyphArray[i]))
|
||||
intersect_glyphs << glyphArray[i];
|
||||
}
|
||||
|
||||
unsigned cost () const { return hb_bit_storage ((unsigned) glyphArray.len); /* bsearch cost */ }
|
||||
|
||||
template <typename set_t>
|
||||
bool collect_coverage (set_t *glyphs) const
|
||||
{ return glyphs->add_sorted_array (glyphArray.as_array ()); }
|
||||
|
||||
public:
|
||||
/* Older compilers need this to be public. */
|
||||
struct iter_t
|
||||
{
|
||||
void init (const struct CoverageFormat1_3 &c_) { c = &c_; i = 0; }
|
||||
bool __more__ () const { return i < c->glyphArray.len; }
|
||||
void __next__ () { i++; }
|
||||
hb_codepoint_t get_glyph () const { return c->glyphArray[i]; }
|
||||
bool operator != (const iter_t& o) const
|
||||
{ return i != o.i; }
|
||||
iter_t __end__ () const { iter_t it; it.init (*c); it.i = c->glyphArray.len; return it; }
|
||||
|
||||
private:
|
||||
const struct CoverageFormat1_3 *c;
|
||||
unsigned int i;
|
||||
};
|
||||
private:
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // #ifndef OT_LAYOUT_COMMON_COVERAGEFORMAT1_HH
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright © 2007,2008,2009 Red Hat, Inc.
|
||||
* Copyright © 2010,2012 Google, Inc.
|
||||
*
|
||||
* This is part of HarfBuzz, a text shaping library.
|
||||
*
|
||||
* Permission is hereby granted, without written agreement and without
|
||||
* license or royalty fees, to use, copy, modify, and distribute this
|
||||
* software and its documentation for any purpose, provided that the
|
||||
* above copyright notice and the following two paragraphs appear in
|
||||
* all copies of this software.
|
||||
*
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*
|
||||
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
*
|
||||
* Red Hat Author(s): Behdad Esfahbod
|
||||
* Google Author(s): Behdad Esfahbod, Garret Rieger
|
||||
*/
|
||||
|
||||
#ifndef OT_LAYOUT_COMMON_COVERAGEFORMAT2_HH
|
||||
#define OT_LAYOUT_COMMON_COVERAGEFORMAT2_HH
|
||||
|
||||
#include "RangeRecord.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace Common {
|
||||
|
||||
template <typename Types>
|
||||
struct CoverageFormat2_4
|
||||
{
|
||||
friend struct Coverage;
|
||||
|
||||
protected:
|
||||
HBUINT16 coverageFormat; /* Format identifier--format = 2 */
|
||||
SortedArray16Of<RangeRecord<Types>>
|
||||
rangeRecord; /* Array of glyph ranges--ordered by
|
||||
* Start GlyphID. rangeCount entries
|
||||
* long */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (4, rangeRecord);
|
||||
|
||||
private:
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (rangeRecord.sanitize (c));
|
||||
}
|
||||
|
||||
unsigned int get_coverage (hb_codepoint_t glyph_id) const
|
||||
{
|
||||
const RangeRecord<Types> &range = rangeRecord.bsearch (glyph_id);
|
||||
return likely (range.first <= range.last)
|
||||
? (unsigned int) range.value + (glyph_id - range.first)
|
||||
: NOT_COVERED;
|
||||
}
|
||||
|
||||
unsigned get_population () const
|
||||
{
|
||||
typename Types::large_int ret = 0;
|
||||
for (const auto &r : rangeRecord)
|
||||
ret += r.get_population ();
|
||||
return ret > UINT_MAX ? UINT_MAX : (unsigned) ret;
|
||||
}
|
||||
|
||||
template <typename Iterator,
|
||||
hb_requires (hb_is_sorted_source_of (Iterator, hb_codepoint_t))>
|
||||
bool serialize (hb_serialize_context_t *c, Iterator glyphs)
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
if (unlikely (!c->extend_min (this))) return_trace (false);
|
||||
|
||||
unsigned num_ranges = 0;
|
||||
hb_codepoint_t last = (hb_codepoint_t) -2;
|
||||
for (auto g: glyphs)
|
||||
{
|
||||
if (last + 1 != g)
|
||||
num_ranges++;
|
||||
last = g;
|
||||
}
|
||||
|
||||
if (unlikely (!rangeRecord.serialize (c, num_ranges))) return_trace (false);
|
||||
if (!num_ranges) return_trace (true);
|
||||
|
||||
unsigned count = 0;
|
||||
unsigned range = (unsigned) -1;
|
||||
last = (hb_codepoint_t) -2;
|
||||
unsigned unsorted = false;
|
||||
for (auto g: glyphs)
|
||||
{
|
||||
if (last + 1 != g)
|
||||
{
|
||||
if (unlikely (last != (hb_codepoint_t) -2 && last + 1 > g))
|
||||
unsorted = true;
|
||||
|
||||
range++;
|
||||
rangeRecord.arrayZ[range].first = g;
|
||||
rangeRecord.arrayZ[range].value = count;
|
||||
}
|
||||
rangeRecord.arrayZ[range].last = g;
|
||||
last = g;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (unlikely (unsorted))
|
||||
rangeRecord.as_array ().qsort (RangeRecord<Types>::cmp_range);
|
||||
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
if (rangeRecord.len > glyphs->get_population () * hb_bit_storage ((unsigned) rangeRecord.len))
|
||||
{
|
||||
for (auto g : *glyphs)
|
||||
if (get_coverage (g) != NOT_COVERED)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return hb_any (+ hb_iter (rangeRecord)
|
||||
| hb_map ([glyphs] (const RangeRecord<Types> &range) { return range.intersects (*glyphs); }));
|
||||
}
|
||||
bool intersects_coverage (const hb_set_t *glyphs, unsigned int index) const
|
||||
{
|
||||
auto *range = rangeRecord.as_array ().bsearch (index);
|
||||
if (range)
|
||||
return range->intersects (*glyphs);
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename IterableOut,
|
||||
hb_requires (hb_is_sink_of (IterableOut, hb_codepoint_t))>
|
||||
void intersect_set (const hb_set_t &glyphs, IterableOut&& intersect_glyphs) const
|
||||
{
|
||||
/* Break out of loop for overlapping, broken, tables,
|
||||
* to avoid fuzzer timouts. */
|
||||
hb_codepoint_t last = 0;
|
||||
for (const auto& range : rangeRecord)
|
||||
{
|
||||
if (unlikely (range.first < last))
|
||||
break;
|
||||
last = range.last;
|
||||
for (hb_codepoint_t g = range.first - 1;
|
||||
glyphs.next (&g) && g <= last;)
|
||||
intersect_glyphs << g;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned cost () const { return hb_bit_storage ((unsigned) rangeRecord.len); /* bsearch cost */ }
|
||||
|
||||
template <typename set_t>
|
||||
bool collect_coverage (set_t *glyphs) const
|
||||
{
|
||||
for (const auto& range: rangeRecord)
|
||||
if (unlikely (!range.collect_coverage (glyphs)))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
/* Older compilers need this to be public. */
|
||||
struct iter_t
|
||||
{
|
||||
void init (const CoverageFormat2_4 &c_)
|
||||
{
|
||||
c = &c_;
|
||||
coverage = 0;
|
||||
i = 0;
|
||||
j = c->rangeRecord.len ? c->rangeRecord[0].first : 0;
|
||||
if (unlikely (c->rangeRecord[0].first > c->rangeRecord[0].last))
|
||||
{
|
||||
/* Broken table. Skip. */
|
||||
i = c->rangeRecord.len;
|
||||
j = 0;
|
||||
}
|
||||
}
|
||||
bool __more__ () const { return i < c->rangeRecord.len; }
|
||||
void __next__ ()
|
||||
{
|
||||
if (j >= c->rangeRecord[i].last)
|
||||
{
|
||||
i++;
|
||||
if (__more__ ())
|
||||
{
|
||||
unsigned int old = coverage;
|
||||
j = c->rangeRecord.arrayZ[i].first;
|
||||
coverage = c->rangeRecord.arrayZ[i].value;
|
||||
if (unlikely (coverage != old + 1))
|
||||
{
|
||||
/* Broken table. Skip. Important to avoid DoS.
|
||||
* Also, our callers depend on coverage being
|
||||
* consecutive and monotonically increasing,
|
||||
* ie. iota(). */
|
||||
i = c->rangeRecord.len;
|
||||
j = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
j = 0;
|
||||
return;
|
||||
}
|
||||
coverage++;
|
||||
j++;
|
||||
}
|
||||
hb_codepoint_t get_glyph () const { return j; }
|
||||
bool operator != (const iter_t& o) const
|
||||
{ return i != o.i || j != o.j; }
|
||||
iter_t __end__ () const
|
||||
{
|
||||
iter_t it;
|
||||
it.init (*c);
|
||||
it.i = c->rangeRecord.len;
|
||||
it.j = 0;
|
||||
return it;
|
||||
}
|
||||
|
||||
private:
|
||||
const struct CoverageFormat2_4 *c;
|
||||
unsigned int i, coverage;
|
||||
hb_codepoint_t j;
|
||||
};
|
||||
private:
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // #ifndef OT_LAYOUT_COMMON_COVERAGEFORMAT2_HH
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright © 2007,2008,2009 Red Hat, Inc.
|
||||
* Copyright © 2010,2012 Google, Inc.
|
||||
*
|
||||
* This is part of HarfBuzz, a text shaping library.
|
||||
*
|
||||
* Permission is hereby granted, without written agreement and without
|
||||
* license or royalty fees, to use, copy, modify, and distribute this
|
||||
* software and its documentation for any purpose, provided that the
|
||||
* above copyright notice and the following two paragraphs appear in
|
||||
* all copies of this software.
|
||||
*
|
||||
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
||||
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
||||
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*
|
||||
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
||||
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
||||
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
||||
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
*
|
||||
* Red Hat Author(s): Behdad Esfahbod
|
||||
* Google Author(s): Behdad Esfahbod, Garret Rieger
|
||||
*/
|
||||
|
||||
#ifndef OT_LAYOUT_COMMON_RANGERECORD_HH
|
||||
#define OT_LAYOUT_COMMON_RANGERECORD_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace Common {
|
||||
|
||||
template <typename Types>
|
||||
struct RangeRecord
|
||||
{
|
||||
typename Types::HBGlyphID first; /* First GlyphID in the range */
|
||||
typename Types::HBGlyphID last; /* Last GlyphID in the range */
|
||||
HBUINT16 value; /* Value */
|
||||
|
||||
DEFINE_SIZE_STATIC (2 + 2 * Types::size);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this));
|
||||
}
|
||||
|
||||
int cmp (hb_codepoint_t g) const
|
||||
{ return g < first ? -1 : g <= last ? 0 : +1; }
|
||||
|
||||
HB_INTERNAL static int cmp_range (const void *pa, const void *pb) {
|
||||
const RangeRecord *a = (const RangeRecord *) pa;
|
||||
const RangeRecord *b = (const RangeRecord *) pb;
|
||||
if (a->first < b->first) return -1;
|
||||
if (a->first > b->first) return +1;
|
||||
if (a->last < b->last) return -1;
|
||||
if (a->last > b->last) return +1;
|
||||
if (a->value < b->value) return -1;
|
||||
if (a->value > b->value) return +1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsigned get_population () const
|
||||
{
|
||||
if (unlikely (last < first)) return 0;
|
||||
return (last - first + 1);
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t &glyphs) const
|
||||
{ return glyphs.intersects (first, last); }
|
||||
|
||||
template <typename set_t>
|
||||
bool collect_coverage (set_t *glyphs) const
|
||||
{ return glyphs->add_range (first, last); }
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(garretrieger): This was previously implemented using
|
||||
// DECLARE_NULL_NAMESPACE_BYTES_TEMPLATE1 (OT, RangeRecord, 9);
|
||||
// but that only works when there is only a single namespace level.
|
||||
// The macro should probably be fixed so it can work in this situation.
|
||||
extern HB_INTERNAL const unsigned char _hb_Null_OT_RangeRecord[9];
|
||||
template <typename Spec>
|
||||
struct Null<OT::Layout::Common::RangeRecord<Spec>> {
|
||||
static OT::Layout::Common::RangeRecord<Spec> const & get_null () {
|
||||
return *reinterpret_cast<const OT::Layout::Common::RangeRecord<Spec> *> (_hb_Null_OT_RangeRecord);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#endif // #ifndef OT_LAYOUT_COMMON_RANGERECORD_HH
|
||||
+1044
File diff suppressed because it is too large.
Load diff
+84
@@ -0,0 +1,84 @@
|
||||
#ifndef OT_LAYOUT_GPOS_ANCHOR_HH
|
||||
#define OT_LAYOUT_GPOS_ANCHOR_HH
|
||||
|
||||
#include "AnchorFormat1.hh"
|
||||
#include "AnchorFormat2.hh"
|
||||
#include "AnchorFormat3.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct Anchor
|
||||
{
|
||||
protected:
|
||||
union {
|
||||
HBUINT16 format; /* Format identifier */
|
||||
AnchorFormat1 format1;
|
||||
AnchorFormat2 format2;
|
||||
AnchorFormat3 format3;
|
||||
} u;
|
||||
public:
|
||||
DEFINE_SIZE_UNION (2, format);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
if (!u.format.sanitize (c)) return_trace (false);
|
||||
hb_barrier ();
|
||||
switch (u.format) {
|
||||
case 1: return_trace (u.format1.sanitize (c));
|
||||
case 2: return_trace (u.format2.sanitize (c));
|
||||
case 3: return_trace (u.format3.sanitize (c));
|
||||
default:return_trace (true);
|
||||
}
|
||||
}
|
||||
|
||||
void get_anchor (hb_ot_apply_context_t *c, hb_codepoint_t glyph_id,
|
||||
float *x, float *y) const
|
||||
{
|
||||
*x = *y = 0;
|
||||
switch (u.format) {
|
||||
case 1: u.format1.get_anchor (c, glyph_id, x, y); return;
|
||||
case 2: u.format2.get_anchor (c, glyph_id, x, y); return;
|
||||
case 3: u.format3.get_anchor (c, glyph_id, x, y); return;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
switch (u.format) {
|
||||
case 1: return_trace (bool (reinterpret_cast<Anchor *> (u.format1.copy (c->serializer))));
|
||||
case 2:
|
||||
if (c->plan->flags & HB_SUBSET_FLAGS_NO_HINTING)
|
||||
{
|
||||
// AnchorFormat 2 just containins extra hinting information, so
|
||||
// if hints are being dropped convert to format 1.
|
||||
return_trace (bool (reinterpret_cast<Anchor *> (u.format1.copy (c->serializer))));
|
||||
}
|
||||
return_trace (bool (reinterpret_cast<Anchor *> (u.format2.copy (c->serializer))));
|
||||
case 3: return_trace (u.format3.subset (c));
|
||||
default:return_trace (false);
|
||||
}
|
||||
}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
switch (u.format) {
|
||||
case 1: case 2:
|
||||
return;
|
||||
case 3:
|
||||
u.format3.collect_variation_indices (c);
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_ANCHOR_HH
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef OT_LAYOUT_GPOS_ANCHORFORMAT1_HH
|
||||
#define OT_LAYOUT_GPOS_ANCHORFORMAT1_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct AnchorFormat1
|
||||
{
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 1 */
|
||||
FWORD xCoordinate; /* Horizontal value--in design units */
|
||||
FWORD yCoordinate; /* Vertical value--in design units */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (6);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this));
|
||||
}
|
||||
|
||||
void get_anchor (hb_ot_apply_context_t *c, hb_codepoint_t glyph_id HB_UNUSED,
|
||||
float *x, float *y) const
|
||||
{
|
||||
hb_font_t *font = c->font;
|
||||
*x = font->em_fscale_x (xCoordinate);
|
||||
*y = font->em_fscale_y (yCoordinate);
|
||||
}
|
||||
|
||||
AnchorFormat1* copy (hb_serialize_context_t *c) const
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
AnchorFormat1* out = c->embed<AnchorFormat1> (this);
|
||||
if (!out) return_trace (out);
|
||||
out->format = 1;
|
||||
return_trace (out);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_ANCHORFORMAT1_HH
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef OT_LAYOUT_GPOS_ANCHORFORMAT2_HH
|
||||
#define OT_LAYOUT_GPOS_ANCHORFORMAT2_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct AnchorFormat2
|
||||
{
|
||||
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 2 */
|
||||
FWORD xCoordinate; /* Horizontal value--in design units */
|
||||
FWORD yCoordinate; /* Vertical value--in design units */
|
||||
HBUINT16 anchorPoint; /* Index to glyph contour point */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (8);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this));
|
||||
}
|
||||
|
||||
void get_anchor (hb_ot_apply_context_t *c, hb_codepoint_t glyph_id,
|
||||
float *x, float *y) const
|
||||
{
|
||||
hb_font_t *font = c->font;
|
||||
|
||||
#ifdef HB_NO_HINTING
|
||||
*x = font->em_fscale_x (xCoordinate);
|
||||
*y = font->em_fscale_y (yCoordinate);
|
||||
return;
|
||||
#endif
|
||||
|
||||
unsigned int x_ppem = font->x_ppem;
|
||||
unsigned int y_ppem = font->y_ppem;
|
||||
hb_position_t cx = 0, cy = 0;
|
||||
bool ret;
|
||||
|
||||
ret = (x_ppem || y_ppem) &&
|
||||
font->get_glyph_contour_point_for_origin (glyph_id, anchorPoint, HB_DIRECTION_LTR, &cx, &cy);
|
||||
*x = ret && x_ppem ? cx : font->em_fscale_x (xCoordinate);
|
||||
*y = ret && y_ppem ? cy : font->em_fscale_y (yCoordinate);
|
||||
}
|
||||
|
||||
AnchorFormat2* copy (hb_serialize_context_t *c) const
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
return_trace (c->embed<AnchorFormat2> (this));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_ANCHORFORMAT2_HH
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
#ifndef OT_LAYOUT_GPOS_ANCHORFORMAT3_HH
|
||||
#define OT_LAYOUT_GPOS_ANCHORFORMAT3_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct AnchorFormat3
|
||||
{
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 3 */
|
||||
FWORD xCoordinate; /* Horizontal value--in design units */
|
||||
FWORD yCoordinate; /* Vertical value--in design units */
|
||||
Offset16To<Device>
|
||||
xDeviceTable; /* Offset to Device table for X
|
||||
* coordinate-- from beginning of
|
||||
* Anchor table (may be NULL) */
|
||||
Offset16To<Device>
|
||||
yDeviceTable; /* Offset to Device table for Y
|
||||
* coordinate-- from beginning of
|
||||
* Anchor table (may be NULL) */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (10);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
if (unlikely (!c->check_struct (this))) return_trace (false);
|
||||
|
||||
return_trace (xDeviceTable.sanitize (c, this) && yDeviceTable.sanitize (c, this));
|
||||
}
|
||||
|
||||
void get_anchor (hb_ot_apply_context_t *c, hb_codepoint_t glyph_id HB_UNUSED,
|
||||
float *x, float *y) const
|
||||
{
|
||||
hb_font_t *font = c->font;
|
||||
*x = font->em_fscale_x (xCoordinate);
|
||||
*y = font->em_fscale_y (yCoordinate);
|
||||
|
||||
if ((font->x_ppem || font->num_coords) && xDeviceTable.sanitize (&c->sanitizer, this))
|
||||
{
|
||||
hb_barrier ();
|
||||
*x += (this+xDeviceTable).get_x_delta (font, c->var_store, c->var_store_cache);
|
||||
}
|
||||
if ((font->y_ppem || font->num_coords) && yDeviceTable.sanitize (&c->sanitizer, this))
|
||||
{
|
||||
hb_barrier ();
|
||||
*y += (this+yDeviceTable).get_y_delta (font, c->var_store, c->var_store_cache);
|
||||
}
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
if (unlikely (!c->serializer->embed (format))) return_trace (false);
|
||||
if (unlikely (!c->serializer->embed (xCoordinate))) return_trace (false);
|
||||
if (unlikely (!c->serializer->embed (yCoordinate))) return_trace (false);
|
||||
|
||||
unsigned x_varidx = xDeviceTable ? (this+xDeviceTable).get_variation_index () : HB_OT_LAYOUT_NO_VARIATIONS_INDEX;
|
||||
if (x_varidx != HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
||||
{
|
||||
hb_pair_t<unsigned, int> *new_varidx_delta;
|
||||
if (!c->plan->layout_variation_idx_delta_map.has (x_varidx, &new_varidx_delta))
|
||||
return_trace (false);
|
||||
|
||||
x_varidx = hb_first (*new_varidx_delta);
|
||||
int delta = hb_second (*new_varidx_delta);
|
||||
if (delta != 0)
|
||||
{
|
||||
if (!c->serializer->check_assign (out->xCoordinate, xCoordinate + delta,
|
||||
HB_SERIALIZE_ERROR_INT_OVERFLOW))
|
||||
return_trace (false);
|
||||
}
|
||||
}
|
||||
|
||||
unsigned y_varidx = yDeviceTable ? (this+yDeviceTable).get_variation_index () : HB_OT_LAYOUT_NO_VARIATIONS_INDEX;
|
||||
if (y_varidx != HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
||||
{
|
||||
hb_pair_t<unsigned, int> *new_varidx_delta;
|
||||
if (!c->plan->layout_variation_idx_delta_map.has (y_varidx, &new_varidx_delta))
|
||||
return_trace (false);
|
||||
|
||||
y_varidx = hb_first (*new_varidx_delta);
|
||||
int delta = hb_second (*new_varidx_delta);
|
||||
if (delta != 0)
|
||||
{
|
||||
if (!c->serializer->check_assign (out->yCoordinate, yCoordinate + delta,
|
||||
HB_SERIALIZE_ERROR_INT_OVERFLOW))
|
||||
return_trace (false);
|
||||
}
|
||||
}
|
||||
|
||||
/* in case that all axes are pinned or no variations after instantiation,
|
||||
* both var_idxes will be mapped to HB_OT_LAYOUT_NO_VARIATIONS_INDEX */
|
||||
if (x_varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX &&
|
||||
y_varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
||||
return_trace (c->serializer->check_assign (out->format, 1, HB_SERIALIZE_ERROR_INT_OVERFLOW));
|
||||
|
||||
if (!c->serializer->embed (xDeviceTable)) return_trace (false);
|
||||
if (!c->serializer->embed (yDeviceTable)) return_trace (false);
|
||||
|
||||
out->xDeviceTable.serialize_copy (c->serializer, xDeviceTable, this, 0, hb_serialize_context_t::Head, &c->plan->layout_variation_idx_delta_map);
|
||||
out->yDeviceTable.serialize_copy (c->serializer, yDeviceTable, this, 0, hb_serialize_context_t::Head, &c->plan->layout_variation_idx_delta_map);
|
||||
return_trace (out);
|
||||
}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
(this+xDeviceTable).collect_variation_indices (c);
|
||||
(this+yDeviceTable).collect_variation_indices (c);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_ANCHORFORMAT3_HH
|
||||
@@ -0,0 +1,87 @@
|
||||
#ifndef OT_LAYOUT_GPOS_ANCHORMATRIX_HH
|
||||
#define OT_LAYOUT_GPOS_ANCHORMATRIX_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct AnchorMatrix
|
||||
{
|
||||
HBUINT16 rows; /* Number of rows */
|
||||
UnsizedArrayOf<Offset16To<Anchor, AnchorMatrix>>
|
||||
matrixZ; /* Matrix of offsets to Anchor tables--
|
||||
* from beginning of AnchorMatrix table */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (2, matrixZ);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c, unsigned int cols) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
if (!c->check_struct (this)) return_trace (false);
|
||||
hb_barrier ();
|
||||
if (unlikely (hb_unsigned_mul_overflows (rows, cols))) return_trace (false);
|
||||
unsigned int count = rows * cols;
|
||||
if (!c->check_array (matrixZ.arrayZ, count)) return_trace (false);
|
||||
|
||||
if (c->lazy_some_gpos)
|
||||
return_trace (true);
|
||||
|
||||
hb_barrier ();
|
||||
for (unsigned int i = 0; i < count; i++)
|
||||
if (!matrixZ[i].sanitize (c, this)) return_trace (false);
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
const Anchor& get_anchor (hb_ot_apply_context_t *c,
|
||||
unsigned int row, unsigned int col,
|
||||
unsigned int cols, bool *found) const
|
||||
{
|
||||
*found = false;
|
||||
if (unlikely (row >= rows || col >= cols)) return Null (Anchor);
|
||||
auto &offset = matrixZ[row * cols + col];
|
||||
if (unlikely (!offset.sanitize (&c->sanitizer, this))) return Null (Anchor);
|
||||
hb_barrier ();
|
||||
*found = !offset.is_null ();
|
||||
return this+offset;
|
||||
}
|
||||
|
||||
template <typename Iterator,
|
||||
hb_requires (hb_is_iterator (Iterator))>
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c,
|
||||
Iterator index_iter) const
|
||||
{
|
||||
for (unsigned i : index_iter)
|
||||
(this+matrixZ[i]).collect_variation_indices (c);
|
||||
}
|
||||
|
||||
template <typename Iterator,
|
||||
hb_requires (hb_is_iterator (Iterator))>
|
||||
bool subset (hb_subset_context_t *c,
|
||||
unsigned num_rows,
|
||||
Iterator index_iter) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
|
||||
auto *out = c->serializer->start_embed (this);
|
||||
|
||||
if (!index_iter) return_trace (false);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
|
||||
out->rows = num_rows;
|
||||
for (const unsigned i : index_iter)
|
||||
{
|
||||
auto *offset = c->serializer->embed (matrixZ[i]);
|
||||
if (!offset) return_trace (false);
|
||||
offset->serialize_subset (c, matrixZ[i], this);
|
||||
}
|
||||
|
||||
return_trace (true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_ANCHORMATRIX_HH */
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef OT_LAYOUT_GPOS_CHAINCONTEXTPOS_HH
|
||||
#define OT_LAYOUT_GPOS_CHAINCONTEXTPOS_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct ChainContextPos : ChainContext {};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_CHAINCONTEXTPOS_HH */
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#ifndef OT_LAYOUT_GPOS_COMMON_HH
|
||||
#define OT_LAYOUT_GPOS_COMMON_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
enum attach_type_t {
|
||||
ATTACH_TYPE_NONE = 0X00,
|
||||
|
||||
/* Each attachment should be either a mark or a cursive; can't be both. */
|
||||
ATTACH_TYPE_MARK = 0X01,
|
||||
ATTACH_TYPE_CURSIVE = 0X02,
|
||||
};
|
||||
|
||||
/* buffer **position** var allocations */
|
||||
#define attach_chain() var.i16[0] /* glyph to which this attaches to, relative to current glyphs; negative for going back, positive for forward. */
|
||||
#define attach_type() var.u8[2] /* attachment type */
|
||||
/* Note! if attach_chain() is zero, the value of attach_type() is irrelevant. */
|
||||
|
||||
template<typename Iterator, typename SrcLookup>
|
||||
static void SinglePos_serialize (hb_serialize_context_t *c,
|
||||
const SrcLookup *src,
|
||||
Iterator it,
|
||||
const hb_hashmap_t<unsigned, hb_pair_t<unsigned, int>> *layout_variation_idx_delta_map,
|
||||
unsigned new_format);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_COMMON_HH
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef OT_LAYOUT_GPOS_CONTEXTPOS_HH
|
||||
#define OT_LAYOUT_GPOS_CONTEXTPOS_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct ContextPos : Context {};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_CONTEXTPOS_HH */
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef OT_LAYOUT_GPOS_CURSIVEPOS_HH
|
||||
#define OT_LAYOUT_GPOS_CURSIVEPOS_HH
|
||||
|
||||
#include "CursivePosFormat1.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct CursivePos
|
||||
{
|
||||
protected:
|
||||
union {
|
||||
HBUINT16 format; /* Format identifier */
|
||||
CursivePosFormat1 format1;
|
||||
} u;
|
||||
|
||||
public:
|
||||
template <typename context_t, typename ...Ts>
|
||||
typename context_t::return_t dispatch (context_t *c, Ts&&... ds) const
|
||||
{
|
||||
if (unlikely (!c->may_dispatch (this, &u.format))) return c->no_dispatch_return_value ();
|
||||
TRACE_DISPATCH (this, u.format);
|
||||
switch (u.format) {
|
||||
case 1: return_trace (c->dispatch (u.format1, std::forward<Ts> (ds)...));
|
||||
default:return_trace (c->default_return_value ());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_CURSIVEPOS_HH */
|
||||
@@ -0,0 +1,311 @@
|
||||
#ifndef OT_LAYOUT_GPOS_CURSIVEPOSFORMAT1_HH
|
||||
#define OT_LAYOUT_GPOS_CURSIVEPOSFORMAT1_HH
|
||||
|
||||
#include "Anchor.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct EntryExitRecord
|
||||
{
|
||||
friend struct CursivePosFormat1;
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c, const struct CursivePosFormat1 *base) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (entryAnchor.sanitize (c, base) && exitAnchor.sanitize (c, base));
|
||||
}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c,
|
||||
const struct CursivePosFormat1 *src_base) const
|
||||
{
|
||||
(src_base+entryAnchor).collect_variation_indices (c);
|
||||
(src_base+exitAnchor).collect_variation_indices (c);
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c,
|
||||
const struct CursivePosFormat1 *src_base) const
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
auto *out = c->serializer->embed (this);
|
||||
if (unlikely (!out)) return_trace (false);
|
||||
|
||||
bool ret = false;
|
||||
ret |= out->entryAnchor.serialize_subset (c, entryAnchor, src_base);
|
||||
ret |= out->exitAnchor.serialize_subset (c, exitAnchor, src_base);
|
||||
return_trace (ret);
|
||||
}
|
||||
|
||||
protected:
|
||||
Offset16To<Anchor, struct CursivePosFormat1>
|
||||
entryAnchor; /* Offset to EntryAnchor table--from
|
||||
* beginning of CursivePos
|
||||
* subtable--may be NULL */
|
||||
Offset16To<Anchor, struct CursivePosFormat1>
|
||||
exitAnchor; /* Offset to ExitAnchor table--from
|
||||
* beginning of CursivePos
|
||||
* subtable--may be NULL */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (4);
|
||||
};
|
||||
|
||||
static void
|
||||
reverse_cursive_minor_offset (hb_glyph_position_t *pos, unsigned int i, hb_direction_t direction, unsigned int new_parent) {
|
||||
int chain = pos[i].attach_chain(), type = pos[i].attach_type();
|
||||
if (likely (!chain || 0 == (type & ATTACH_TYPE_CURSIVE)))
|
||||
return;
|
||||
|
||||
pos[i].attach_chain() = 0;
|
||||
|
||||
unsigned int j = (int) i + chain;
|
||||
|
||||
/* Stop if we see new parent in the chain. */
|
||||
if (j == new_parent)
|
||||
return;
|
||||
|
||||
reverse_cursive_minor_offset (pos, j, direction, new_parent);
|
||||
|
||||
if (HB_DIRECTION_IS_HORIZONTAL (direction))
|
||||
pos[j].y_offset = -pos[i].y_offset;
|
||||
else
|
||||
pos[j].x_offset = -pos[i].x_offset;
|
||||
|
||||
pos[j].attach_chain() = -chain;
|
||||
pos[j].attach_type() = type;
|
||||
}
|
||||
|
||||
|
||||
struct CursivePosFormat1
|
||||
{
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 1 */
|
||||
Offset16To<Coverage>
|
||||
coverage; /* Offset to Coverage table--from
|
||||
* beginning of subtable */
|
||||
Array16Of<EntryExitRecord>
|
||||
entryExitRecord; /* Array of EntryExit records--in
|
||||
* Coverage Index order */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (6, entryExitRecord);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
if (unlikely (!coverage.sanitize (c, this)))
|
||||
return_trace (false);
|
||||
|
||||
if (c->lazy_some_gpos)
|
||||
return_trace (entryExitRecord.sanitize_shallow (c));
|
||||
else
|
||||
return_trace (entryExitRecord.sanitize (c, this));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{ return (this+coverage).intersects (glyphs); }
|
||||
|
||||
void closure_lookups (hb_closure_lookups_context_t *c) const {}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
+ hb_zip (this+coverage, entryExitRecord)
|
||||
| hb_filter (c->glyph_set, hb_first)
|
||||
| hb_map (hb_second)
|
||||
| hb_apply ([&] (const EntryExitRecord& record) { record.collect_variation_indices (c, this); })
|
||||
;
|
||||
}
|
||||
|
||||
void collect_glyphs (hb_collect_glyphs_context_t *c) const
|
||||
{ if (unlikely (!(this+coverage).collect_coverage (c->input))) return; }
|
||||
|
||||
const Coverage &get_coverage () const { return this+coverage; }
|
||||
|
||||
bool apply (hb_ot_apply_context_t *c) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
hb_buffer_t *buffer = c->buffer;
|
||||
|
||||
const EntryExitRecord &this_record = entryExitRecord[(this+coverage).get_coverage (buffer->cur().codepoint)];
|
||||
if (!this_record.entryAnchor ||
|
||||
unlikely (!this_record.entryAnchor.sanitize (&c->sanitizer, this))) return_trace (false);
|
||||
hb_barrier ();
|
||||
|
||||
auto &skippy_iter = c->iter_input;
|
||||
skippy_iter.reset_fast (buffer->idx);
|
||||
unsigned unsafe_from;
|
||||
if (unlikely (!skippy_iter.prev (&unsafe_from)))
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (unsafe_from, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
const EntryExitRecord &prev_record = entryExitRecord[(this+coverage).get_coverage (buffer->info[skippy_iter.idx].codepoint)];
|
||||
if (!prev_record.exitAnchor ||
|
||||
unlikely (!prev_record.exitAnchor.sanitize (&c->sanitizer, this)))
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (skippy_iter.idx, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
hb_barrier ();
|
||||
|
||||
unsigned int i = skippy_iter.idx;
|
||||
unsigned int j = buffer->idx;
|
||||
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"cursive attaching glyph at %u to glyph at %u",
|
||||
i, j);
|
||||
}
|
||||
|
||||
buffer->unsafe_to_break (i, j + 1);
|
||||
float entry_x, entry_y, exit_x, exit_y;
|
||||
(this+prev_record.exitAnchor).get_anchor (c, buffer->info[i].codepoint, &exit_x, &exit_y);
|
||||
(this+this_record.entryAnchor).get_anchor (c, buffer->info[j].codepoint, &entry_x, &entry_y);
|
||||
|
||||
hb_glyph_position_t *pos = buffer->pos;
|
||||
|
||||
hb_position_t d;
|
||||
/* Main-direction adjustment */
|
||||
switch (c->direction) {
|
||||
case HB_DIRECTION_LTR:
|
||||
pos[i].x_advance = roundf (exit_x) + pos[i].x_offset;
|
||||
|
||||
d = roundf (entry_x) + pos[j].x_offset;
|
||||
pos[j].x_advance -= d;
|
||||
pos[j].x_offset -= d;
|
||||
break;
|
||||
case HB_DIRECTION_RTL:
|
||||
d = roundf (exit_x) + pos[i].x_offset;
|
||||
pos[i].x_advance -= d;
|
||||
pos[i].x_offset -= d;
|
||||
|
||||
pos[j].x_advance = roundf (entry_x) + pos[j].x_offset;
|
||||
break;
|
||||
case HB_DIRECTION_TTB:
|
||||
pos[i].y_advance = roundf (exit_y) + pos[i].y_offset;
|
||||
|
||||
d = roundf (entry_y) + pos[j].y_offset;
|
||||
pos[j].y_advance -= d;
|
||||
pos[j].y_offset -= d;
|
||||
break;
|
||||
case HB_DIRECTION_BTT:
|
||||
d = roundf (exit_y) + pos[i].y_offset;
|
||||
pos[i].y_advance -= d;
|
||||
pos[i].y_offset -= d;
|
||||
|
||||
pos[j].y_advance = roundf (entry_y);
|
||||
break;
|
||||
case HB_DIRECTION_INVALID:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/* Cross-direction adjustment */
|
||||
|
||||
/* We attach child to parent (think graph theory and rooted trees whereas
|
||||
* the root stays on baseline and each node aligns itself against its
|
||||
* parent.
|
||||
*
|
||||
* Optimize things for the case of RightToLeft, as that's most common in
|
||||
* Arabic. */
|
||||
unsigned int child = i;
|
||||
unsigned int parent = j;
|
||||
hb_position_t x_offset = roundf (entry_x - exit_x);
|
||||
hb_position_t y_offset = roundf (entry_y - exit_y);
|
||||
if (!(c->lookup_props & LookupFlag::RightToLeft))
|
||||
{
|
||||
unsigned int k = child;
|
||||
child = parent;
|
||||
parent = k;
|
||||
x_offset = -x_offset;
|
||||
y_offset = -y_offset;
|
||||
}
|
||||
|
||||
/* If child was already connected to someone else, walk through its old
|
||||
* chain and reverse the link direction, such that the whole tree of its
|
||||
* previous connection now attaches to new parent. Watch out for case
|
||||
* where new parent is on the path from old chain...
|
||||
*/
|
||||
reverse_cursive_minor_offset (pos, child, c->direction, parent);
|
||||
|
||||
pos[child].attach_type() = ATTACH_TYPE_CURSIVE;
|
||||
pos[child].attach_chain() = (int) parent - (int) child;
|
||||
buffer->scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
|
||||
if (likely (HB_DIRECTION_IS_HORIZONTAL (c->direction)))
|
||||
pos[child].y_offset = y_offset;
|
||||
else
|
||||
pos[child].x_offset = x_offset;
|
||||
|
||||
/* If parent was attached to child, separate them.
|
||||
* https://github.com/harfbuzz/harfbuzz/issues/2469
|
||||
*/
|
||||
if (unlikely (pos[parent].attach_chain() == -pos[child].attach_chain()))
|
||||
{
|
||||
pos[parent].attach_chain() = 0;
|
||||
if (likely (HB_DIRECTION_IS_HORIZONTAL (c->direction)))
|
||||
pos[parent].y_offset = 0;
|
||||
else
|
||||
pos[parent].x_offset = 0;
|
||||
}
|
||||
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"cursive attached glyph at %u to glyph at %u",
|
||||
i, j);
|
||||
}
|
||||
|
||||
buffer->idx++;
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
template <typename Iterator,
|
||||
hb_requires (hb_is_iterator (Iterator))>
|
||||
void serialize (hb_subset_context_t *c,
|
||||
Iterator it,
|
||||
const struct CursivePosFormat1 *src_base)
|
||||
{
|
||||
if (unlikely (!c->serializer->extend_min ((*this)))) return;
|
||||
this->format = 1;
|
||||
this->entryExitRecord.len = it.len ();
|
||||
|
||||
for (const EntryExitRecord& entry_record : + it
|
||||
| hb_map (hb_second))
|
||||
entry_record.subset (c, src_base);
|
||||
|
||||
auto glyphs =
|
||||
+ it
|
||||
| hb_map_retains_sorting (hb_first)
|
||||
;
|
||||
|
||||
coverage.serialize_serialize (c->serializer, glyphs);
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
const hb_set_t &glyphset = *c->plan->glyphset_gsub ();
|
||||
const hb_map_t &glyph_map = *c->plan->glyph_map;
|
||||
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
|
||||
auto it =
|
||||
+ hb_zip (this+coverage, entryExitRecord)
|
||||
| hb_filter (glyphset, hb_first)
|
||||
| hb_map_retains_sorting ([&] (hb_pair_t<hb_codepoint_t, const EntryExitRecord&> p) -> hb_pair_t<hb_codepoint_t, const EntryExitRecord&>
|
||||
{ return hb_pair (glyph_map[p.first], p.second);})
|
||||
;
|
||||
|
||||
bool ret = bool (it);
|
||||
out->serialize (c, it, this);
|
||||
return_trace (ret);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_CURSIVEPOSFORMAT1_HH */
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef OT_LAYOUT_GPOS_EXTENSIONPOS_HH
|
||||
#define OT_LAYOUT_GPOS_EXTENSIONPOS_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct ExtensionPos : Extension<ExtensionPos>
|
||||
{
|
||||
typedef struct PosLookupSubTable SubTable;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_EXTENSIONPOS_HH */
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
#ifndef OT_LAYOUT_GPOS_GPOS_HH
|
||||
#define OT_LAYOUT_GPOS_GPOS_HH
|
||||
|
||||
#include "../../../hb-ot-layout-common.hh"
|
||||
#include "../../../hb-ot-layout-gsubgpos.hh"
|
||||
#include "Common.hh"
|
||||
#include "PosLookup.hh"
|
||||
|
||||
namespace OT {
|
||||
|
||||
using Layout::GPOS_impl::PosLookup;
|
||||
|
||||
namespace Layout {
|
||||
|
||||
static void
|
||||
propagate_attachment_offsets (hb_glyph_position_t *pos,
|
||||
unsigned int len,
|
||||
unsigned int i,
|
||||
hb_direction_t direction,
|
||||
unsigned nesting_level = HB_MAX_NESTING_LEVEL);
|
||||
|
||||
/*
|
||||
* GPOS -- Glyph Positioning
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gpos
|
||||
*/
|
||||
|
||||
struct GPOS : GSUBGPOS
|
||||
{
|
||||
static constexpr hb_tag_t tableTag = HB_OT_TAG_GPOS;
|
||||
|
||||
using Lookup = PosLookup;
|
||||
|
||||
const PosLookup& get_lookup (unsigned int i) const
|
||||
{ return static_cast<const PosLookup &> (GSUBGPOS::get_lookup (i)); }
|
||||
|
||||
static inline void position_start (hb_font_t *font, hb_buffer_t *buffer);
|
||||
static inline void position_finish_advances (hb_font_t *font, hb_buffer_t *buffer);
|
||||
static inline void position_finish_offsets (hb_font_t *font, hb_buffer_t *buffer);
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
hb_subset_layout_context_t l (c, tableTag);
|
||||
return GSUBGPOS::subset<PosLookup> (&l);
|
||||
}
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (GSUBGPOS::sanitize<PosLookup> (c));
|
||||
}
|
||||
|
||||
HB_INTERNAL bool is_blocklisted (hb_blob_t *blob,
|
||||
hb_face_t *face) const;
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
for (unsigned i = 0; i < GSUBGPOS::get_lookup_count (); i++)
|
||||
{
|
||||
if (!c->gpos_lookups->has (i)) continue;
|
||||
const PosLookup &l = get_lookup (i);
|
||||
l.dispatch (c);
|
||||
}
|
||||
}
|
||||
|
||||
void closure_lookups (hb_face_t *face,
|
||||
const hb_set_t *glyphs,
|
||||
hb_set_t *lookup_indexes /* IN/OUT */) const
|
||||
{ GSUBGPOS::closure_lookups<PosLookup> (face, glyphs, lookup_indexes); }
|
||||
|
||||
typedef GSUBGPOS::accelerator_t<GPOS> accelerator_t;
|
||||
};
|
||||
|
||||
|
||||
static void
|
||||
propagate_attachment_offsets (hb_glyph_position_t *pos,
|
||||
unsigned int len,
|
||||
unsigned int i,
|
||||
hb_direction_t direction,
|
||||
unsigned nesting_level)
|
||||
{
|
||||
/* Adjusts offsets of attached glyphs (both cursive and mark) to accumulate
|
||||
* offset of glyph they are attached to. */
|
||||
int chain = pos[i].attach_chain(), type = pos[i].attach_type();
|
||||
if (likely (!chain))
|
||||
return;
|
||||
|
||||
pos[i].attach_chain() = 0;
|
||||
|
||||
unsigned int j = (int) i + chain;
|
||||
|
||||
if (unlikely (j >= len))
|
||||
return;
|
||||
|
||||
if (unlikely (!nesting_level))
|
||||
return;
|
||||
|
||||
propagate_attachment_offsets (pos, len, j, direction, nesting_level - 1);
|
||||
|
||||
assert (!!(type & GPOS_impl::ATTACH_TYPE_MARK) ^ !!(type & GPOS_impl::ATTACH_TYPE_CURSIVE));
|
||||
|
||||
if (type & GPOS_impl::ATTACH_TYPE_CURSIVE)
|
||||
{
|
||||
if (HB_DIRECTION_IS_HORIZONTAL (direction))
|
||||
pos[i].y_offset += pos[j].y_offset;
|
||||
else
|
||||
pos[i].x_offset += pos[j].x_offset;
|
||||
}
|
||||
else /*if (type & GPOS_impl::ATTACH_TYPE_MARK)*/
|
||||
{
|
||||
pos[i].x_offset += pos[j].x_offset;
|
||||
pos[i].y_offset += pos[j].y_offset;
|
||||
|
||||
assert (j < i);
|
||||
if (HB_DIRECTION_IS_FORWARD (direction))
|
||||
for (unsigned int k = j; k < i; k++) {
|
||||
pos[i].x_offset -= pos[k].x_advance;
|
||||
pos[i].y_offset -= pos[k].y_advance;
|
||||
}
|
||||
else
|
||||
for (unsigned int k = j + 1; k < i + 1; k++) {
|
||||
pos[i].x_offset += pos[k].x_advance;
|
||||
pos[i].y_offset += pos[k].y_advance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GPOS::position_start (hb_font_t *font HB_UNUSED, hb_buffer_t *buffer)
|
||||
{
|
||||
unsigned int count = buffer->len;
|
||||
for (unsigned int i = 0; i < count; i++)
|
||||
buffer->pos[i].attach_chain() = buffer->pos[i].attach_type() = 0;
|
||||
}
|
||||
|
||||
void
|
||||
GPOS::position_finish_advances (hb_font_t *font HB_UNUSED, hb_buffer_t *buffer HB_UNUSED)
|
||||
{
|
||||
//_hb_buffer_assert_gsubgpos_vars (buffer);
|
||||
}
|
||||
|
||||
void
|
||||
GPOS::position_finish_offsets (hb_font_t *font, hb_buffer_t *buffer)
|
||||
{
|
||||
_hb_buffer_assert_gsubgpos_vars (buffer);
|
||||
|
||||
unsigned int len;
|
||||
hb_glyph_position_t *pos = hb_buffer_get_glyph_positions (buffer, &len);
|
||||
hb_direction_t direction = buffer->props.direction;
|
||||
|
||||
/* Handle attachments */
|
||||
if (buffer->scratch_flags & HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT)
|
||||
for (unsigned i = 0; i < len; i++)
|
||||
propagate_attachment_offsets (pos, len, i, direction);
|
||||
|
||||
if (unlikely (font->slant_xy) &&
|
||||
HB_DIRECTION_IS_HORIZONTAL (direction))
|
||||
{
|
||||
/* Slanting shaping results is only supported for horizontal text,
|
||||
* as it gets weird otherwise. */
|
||||
for (unsigned i = 0; i < len; i++)
|
||||
if (unlikely (pos[i].y_offset))
|
||||
pos[i].x_offset += roundf (font->slant_xy * pos[i].y_offset);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct GPOS_accelerator_t : Layout::GPOS::accelerator_t {
|
||||
GPOS_accelerator_t (hb_face_t *face) : Layout::GPOS::accelerator_t (face) {}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_GPOS_HH */
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef OT_LAYOUT_GPOS_LIGATUREARRAY_HH
|
||||
#define OT_LAYOUT_GPOS_LIGATUREARRAY_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
|
||||
typedef AnchorMatrix LigatureAttach; /* component-major--
|
||||
* in order of writing direction--,
|
||||
* mark-minor--
|
||||
* ordered by class--zero-based. */
|
||||
|
||||
/* Array of LigatureAttach tables ordered by LigatureCoverage Index */
|
||||
struct LigatureArray : List16OfOffset16To<LigatureAttach>
|
||||
{
|
||||
template <typename Iterator,
|
||||
hb_requires (hb_is_iterator (Iterator))>
|
||||
bool subset (hb_subset_context_t *c,
|
||||
Iterator coverage,
|
||||
unsigned class_count,
|
||||
const hb_map_t *klass_mapping) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
const hb_set_t &glyphset = *c->plan->glyphset_gsub ();
|
||||
|
||||
auto *out = c->serializer->start_embed (this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
|
||||
bool ret = false;
|
||||
for (const auto _ : + hb_zip (coverage, *this)
|
||||
| hb_filter (glyphset, hb_first))
|
||||
{
|
||||
auto *matrix = out->serialize_append (c->serializer);
|
||||
if (unlikely (!matrix)) return_trace (false);
|
||||
|
||||
const LigatureAttach& src = (this + _.second);
|
||||
auto indexes =
|
||||
+ hb_range (src.rows * class_count)
|
||||
| hb_filter ([=] (unsigned index) { return klass_mapping->has (index % class_count); })
|
||||
;
|
||||
ret |= matrix->serialize_subset (c,
|
||||
_.second,
|
||||
this,
|
||||
src.rows,
|
||||
indexes);
|
||||
}
|
||||
return_trace (ret);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_LIGATUREARRAY_HH */
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
#ifndef OT_LAYOUT_GPOS_MARKARRAY_HH
|
||||
#define OT_LAYOUT_GPOS_MARKARRAY_HH
|
||||
|
||||
#include "AnchorMatrix.hh"
|
||||
#include "MarkRecord.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct MarkArray : Array16Of<MarkRecord> /* Array of MarkRecords--in Coverage order */
|
||||
{
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (Array16Of<MarkRecord>::sanitize (c, this));
|
||||
}
|
||||
|
||||
bool apply (hb_ot_apply_context_t *c,
|
||||
unsigned int mark_index, unsigned int glyph_index,
|
||||
const AnchorMatrix &anchors, unsigned int class_count,
|
||||
unsigned int glyph_pos) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
hb_buffer_t *buffer = c->buffer;
|
||||
const MarkRecord &record = Array16Of<MarkRecord>::operator[](mark_index);
|
||||
unsigned int mark_class = record.klass;
|
||||
|
||||
const Anchor& mark_anchor = this + record.markAnchor;
|
||||
bool found;
|
||||
const Anchor& glyph_anchor = anchors.get_anchor (c, glyph_index, mark_class, class_count, &found);
|
||||
/* If this subtable doesn't have an anchor for this base and this class,
|
||||
* return false such that the subsequent subtables have a chance at it. */
|
||||
if (unlikely (!found)) return_trace (false);
|
||||
|
||||
float mark_x, mark_y, base_x, base_y;
|
||||
|
||||
buffer->unsafe_to_break (glyph_pos, buffer->idx + 1);
|
||||
mark_anchor.get_anchor (c, buffer->cur().codepoint, &mark_x, &mark_y);
|
||||
glyph_anchor.get_anchor (c, buffer->info[glyph_pos].codepoint, &base_x, &base_y);
|
||||
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"attaching mark glyph at %u to glyph at %u",
|
||||
c->buffer->idx, glyph_pos);
|
||||
}
|
||||
|
||||
hb_glyph_position_t &o = buffer->cur_pos();
|
||||
o.x_offset = roundf (base_x - mark_x);
|
||||
o.y_offset = roundf (base_y - mark_y);
|
||||
o.attach_type() = ATTACH_TYPE_MARK;
|
||||
o.attach_chain() = (int) glyph_pos - (int) buffer->idx;
|
||||
buffer->scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
|
||||
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"attached mark glyph at %u to glyph at %u",
|
||||
c->buffer->idx, glyph_pos);
|
||||
}
|
||||
|
||||
buffer->idx++;
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
template <typename Iterator,
|
||||
hb_requires (hb_is_iterator (Iterator))>
|
||||
bool subset (hb_subset_context_t *c,
|
||||
Iterator coverage,
|
||||
const hb_map_t *klass_mapping) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
const hb_set_t &glyphset = *c->plan->glyphset_gsub ();
|
||||
|
||||
auto* out = c->serializer->start_embed (this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
|
||||
auto mark_iter =
|
||||
+ hb_zip (coverage, this->iter ())
|
||||
| hb_filter (glyphset, hb_first)
|
||||
| hb_map (hb_second)
|
||||
;
|
||||
|
||||
bool ret = false;
|
||||
unsigned new_length = 0;
|
||||
for (const auto& mark_record : mark_iter) {
|
||||
ret |= mark_record.subset (c, this, klass_mapping);
|
||||
new_length++;
|
||||
}
|
||||
|
||||
if (unlikely (!c->serializer->check_assign (out->len, new_length,
|
||||
HB_SERIALIZE_ERROR_ARRAY_OVERFLOW)))
|
||||
return_trace (false);
|
||||
|
||||
return_trace (ret);
|
||||
}
|
||||
};
|
||||
|
||||
HB_INTERNAL inline
|
||||
void Markclass_closure_and_remap_indexes (const Coverage &mark_coverage,
|
||||
const MarkArray &mark_array,
|
||||
const hb_set_t &glyphset,
|
||||
hb_map_t* klass_mapping /* INOUT */)
|
||||
{
|
||||
hb_set_t orig_classes;
|
||||
|
||||
+ hb_zip (mark_coverage, mark_array)
|
||||
| hb_filter (glyphset, hb_first)
|
||||
| hb_map (hb_second)
|
||||
| hb_map (&MarkRecord::get_class)
|
||||
| hb_sink (orig_classes)
|
||||
;
|
||||
|
||||
unsigned idx = 0;
|
||||
for (auto klass : orig_classes.iter ())
|
||||
{
|
||||
if (klass_mapping->has (klass)) continue;
|
||||
klass_mapping->set (klass, idx);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_MARKARRAY_HH */
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef OT_LAYOUT_GPOS_MARKBASEPOS_HH
|
||||
#define OT_LAYOUT_GPOS_MARKBASEPOS_HH
|
||||
|
||||
#include "MarkBasePosFormat1.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct MarkBasePos
|
||||
{
|
||||
protected:
|
||||
union {
|
||||
HBUINT16 format; /* Format identifier */
|
||||
MarkBasePosFormat1_2<SmallTypes> format1;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
MarkBasePosFormat1_2<MediumTypes> format2;
|
||||
#endif
|
||||
} u;
|
||||
|
||||
public:
|
||||
template <typename context_t, typename ...Ts>
|
||||
typename context_t::return_t dispatch (context_t *c, Ts&&... ds) const
|
||||
{
|
||||
if (unlikely (!c->may_dispatch (this, &u.format))) return c->no_dispatch_return_value ();
|
||||
TRACE_DISPATCH (this, u.format);
|
||||
switch (u.format) {
|
||||
case 1: return_trace (c->dispatch (u.format1, std::forward<Ts> (ds)...));
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 2: return_trace (c->dispatch (u.format2, std::forward<Ts> (ds)...));
|
||||
#endif
|
||||
default:return_trace (c->default_return_value ());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_MARKBASEPOS_HH */
|
||||
@@ -0,0 +1,243 @@
|
||||
#ifndef OT_LAYOUT_GPOS_MARKBASEPOSFORMAT1_HH
|
||||
#define OT_LAYOUT_GPOS_MARKBASEPOSFORMAT1_HH
|
||||
|
||||
#include "MarkArray.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
typedef AnchorMatrix BaseArray; /* base-major--
|
||||
* in order of BaseCoverage Index--,
|
||||
* mark-minor--
|
||||
* ordered by class--zero-based. */
|
||||
|
||||
template <typename Types>
|
||||
struct MarkBasePosFormat1_2
|
||||
{
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 1 */
|
||||
typename Types::template OffsetTo<Coverage>
|
||||
markCoverage; /* Offset to MarkCoverage table--from
|
||||
* beginning of MarkBasePos subtable */
|
||||
typename Types::template OffsetTo<Coverage>
|
||||
baseCoverage; /* Offset to BaseCoverage table--from
|
||||
* beginning of MarkBasePos subtable */
|
||||
HBUINT16 classCount; /* Number of classes defined for marks */
|
||||
typename Types::template OffsetTo<MarkArray>
|
||||
markArray; /* Offset to MarkArray table--from
|
||||
* beginning of MarkBasePos subtable */
|
||||
typename Types::template OffsetTo<BaseArray>
|
||||
baseArray; /* Offset to BaseArray table--from
|
||||
* beginning of MarkBasePos subtable */
|
||||
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (4 + 4 * Types::size);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this) &&
|
||||
markCoverage.sanitize (c, this) &&
|
||||
baseCoverage.sanitize (c, this) &&
|
||||
markArray.sanitize (c, this) &&
|
||||
baseArray.sanitize (c, this, (unsigned int) classCount));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
return (this+markCoverage).intersects (glyphs) &&
|
||||
(this+baseCoverage).intersects (glyphs);
|
||||
}
|
||||
|
||||
void closure_lookups (hb_closure_lookups_context_t *c) const {}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
+ hb_zip (this+markCoverage, this+markArray)
|
||||
| hb_filter (c->glyph_set, hb_first)
|
||||
| hb_map (hb_second)
|
||||
| hb_apply ([&] (const MarkRecord& record) { record.collect_variation_indices (c, &(this+markArray)); })
|
||||
;
|
||||
|
||||
hb_map_t klass_mapping;
|
||||
Markclass_closure_and_remap_indexes (this+markCoverage, this+markArray, *c->glyph_set, &klass_mapping);
|
||||
|
||||
unsigned basecount = (this+baseArray).rows;
|
||||
auto base_iter =
|
||||
+ hb_zip (this+baseCoverage, hb_range (basecount))
|
||||
| hb_filter (c->glyph_set, hb_first)
|
||||
| hb_map (hb_second)
|
||||
;
|
||||
|
||||
hb_sorted_vector_t<unsigned> base_indexes;
|
||||
for (const unsigned row : base_iter)
|
||||
{
|
||||
+ hb_range ((unsigned) classCount)
|
||||
| hb_filter (klass_mapping)
|
||||
| hb_map ([&] (const unsigned col) { return row * (unsigned) classCount + col; })
|
||||
| hb_sink (base_indexes)
|
||||
;
|
||||
}
|
||||
(this+baseArray).collect_variation_indices (c, base_indexes.iter ());
|
||||
}
|
||||
|
||||
void collect_glyphs (hb_collect_glyphs_context_t *c) const
|
||||
{
|
||||
if (unlikely (!(this+markCoverage).collect_coverage (c->input))) return;
|
||||
if (unlikely (!(this+baseCoverage).collect_coverage (c->input))) return;
|
||||
}
|
||||
|
||||
const Coverage &get_coverage () const { return this+markCoverage; }
|
||||
|
||||
static inline bool accept (hb_buffer_t *buffer, unsigned idx)
|
||||
{
|
||||
/* We only want to attach to the first of a MultipleSubst sequence.
|
||||
* https://github.com/harfbuzz/harfbuzz/issues/740
|
||||
* Reject others...
|
||||
* ...but stop if we find a mark in the MultipleSubst sequence:
|
||||
* https://github.com/harfbuzz/harfbuzz/issues/1020 */
|
||||
return !_hb_glyph_info_multiplied (&buffer->info[idx]) ||
|
||||
0 == _hb_glyph_info_get_lig_comp (&buffer->info[idx]) ||
|
||||
(idx == 0 ||
|
||||
_hb_glyph_info_is_mark (&buffer->info[idx - 1]) ||
|
||||
!_hb_glyph_info_multiplied (&buffer->info[idx - 1]) ||
|
||||
_hb_glyph_info_get_lig_id (&buffer->info[idx]) !=
|
||||
_hb_glyph_info_get_lig_id (&buffer->info[idx - 1]) ||
|
||||
_hb_glyph_info_get_lig_comp (&buffer->info[idx]) !=
|
||||
_hb_glyph_info_get_lig_comp (&buffer->info[idx - 1]) + 1
|
||||
);
|
||||
}
|
||||
|
||||
bool apply (hb_ot_apply_context_t *c) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
hb_buffer_t *buffer = c->buffer;
|
||||
unsigned int mark_index = (this+markCoverage).get_coverage (buffer->cur().codepoint);
|
||||
if (likely (mark_index == NOT_COVERED)) return_trace (false);
|
||||
|
||||
/* Now we search backwards for a non-mark glyph.
|
||||
* We don't use skippy_iter.prev() to avoid O(n^2) behavior. */
|
||||
|
||||
auto &skippy_iter = c->iter_input;
|
||||
skippy_iter.set_lookup_props (LookupFlag::IgnoreMarks);
|
||||
|
||||
if (c->last_base_until > buffer->idx)
|
||||
{
|
||||
c->last_base_until = 0;
|
||||
c->last_base = -1;
|
||||
}
|
||||
unsigned j;
|
||||
for (j = buffer->idx; j > c->last_base_until; j--)
|
||||
{
|
||||
auto match = skippy_iter.match (buffer->info[j - 1]);
|
||||
if (match == skippy_iter.MATCH)
|
||||
{
|
||||
// https://github.com/harfbuzz/harfbuzz/issues/4124
|
||||
if (!accept (buffer, j - 1) &&
|
||||
NOT_COVERED == (this+baseCoverage).get_coverage (buffer->info[j - 1].codepoint))
|
||||
match = skippy_iter.SKIP;
|
||||
}
|
||||
if (match == skippy_iter.MATCH)
|
||||
{
|
||||
c->last_base = (signed) j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
c->last_base_until = buffer->idx;
|
||||
if (c->last_base == -1)
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (0, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
unsigned idx = (unsigned) c->last_base;
|
||||
|
||||
/* Checking that matched glyph is actually a base glyph by GDEF is too strong; disabled */
|
||||
//if (!_hb_glyph_info_is_base_glyph (&buffer->info[idx])) { return_trace (false); }
|
||||
|
||||
unsigned int base_index = (this+baseCoverage).get_coverage (buffer->info[idx].codepoint);
|
||||
if (base_index == NOT_COVERED)
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (idx, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
return_trace ((this+markArray).apply (c, mark_index, base_index, this+baseArray, classCount, idx));
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
const hb_set_t &glyphset = *c->plan->glyphset_gsub ();
|
||||
const hb_map_t &glyph_map = *c->plan->glyph_map;
|
||||
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
out->format = format;
|
||||
|
||||
hb_map_t klass_mapping;
|
||||
Markclass_closure_and_remap_indexes (this+markCoverage, this+markArray, glyphset, &klass_mapping);
|
||||
|
||||
if (!klass_mapping.get_population ()) return_trace (false);
|
||||
out->classCount = klass_mapping.get_population ();
|
||||
|
||||
auto mark_iter =
|
||||
+ hb_zip (this+markCoverage, this+markArray)
|
||||
| hb_filter (glyphset, hb_first)
|
||||
;
|
||||
|
||||
hb_sorted_vector_t<hb_codepoint_t> new_coverage;
|
||||
+ mark_iter
|
||||
| hb_map (hb_first)
|
||||
| hb_map (glyph_map)
|
||||
| hb_sink (new_coverage)
|
||||
;
|
||||
|
||||
if (!out->markCoverage.serialize_serialize (c->serializer, new_coverage.iter ()))
|
||||
return_trace (false);
|
||||
|
||||
if (unlikely (!out->markArray.serialize_subset (c, markArray, this,
|
||||
(this+markCoverage).iter (),
|
||||
&klass_mapping)))
|
||||
return_trace (false);
|
||||
|
||||
unsigned basecount = (this+baseArray).rows;
|
||||
auto base_iter =
|
||||
+ hb_zip (this+baseCoverage, hb_range (basecount))
|
||||
| hb_filter (glyphset, hb_first)
|
||||
;
|
||||
|
||||
new_coverage.reset ();
|
||||
+ base_iter
|
||||
| hb_map (hb_first)
|
||||
| hb_map (glyph_map)
|
||||
| hb_sink (new_coverage)
|
||||
;
|
||||
|
||||
if (!out->baseCoverage.serialize_serialize (c->serializer, new_coverage.iter ()))
|
||||
return_trace (false);
|
||||
|
||||
hb_sorted_vector_t<unsigned> base_indexes;
|
||||
for (const unsigned row : + base_iter
|
||||
| hb_map (hb_second))
|
||||
{
|
||||
+ hb_range ((unsigned) classCount)
|
||||
| hb_filter (klass_mapping)
|
||||
| hb_map ([&] (const unsigned col) { return row * (unsigned) classCount + col; })
|
||||
| hb_sink (base_indexes)
|
||||
;
|
||||
}
|
||||
|
||||
return_trace (out->baseArray.serialize_subset (c, baseArray, this,
|
||||
base_iter.len (),
|
||||
base_indexes.iter ()));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_MARKBASEPOSFORMAT1_HH */
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef OT_LAYOUT_GPOS_MARKLIGPOS_HH
|
||||
#define OT_LAYOUT_GPOS_MARKLIGPOS_HH
|
||||
|
||||
#include "MarkLigPosFormat1.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct MarkLigPos
|
||||
{
|
||||
protected:
|
||||
union {
|
||||
HBUINT16 format; /* Format identifier */
|
||||
MarkLigPosFormat1_2<SmallTypes> format1;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
MarkLigPosFormat1_2<MediumTypes> format2;
|
||||
#endif
|
||||
} u;
|
||||
|
||||
public:
|
||||
template <typename context_t, typename ...Ts>
|
||||
typename context_t::return_t dispatch (context_t *c, Ts&&... ds) const
|
||||
{
|
||||
if (unlikely (!c->may_dispatch (this, &u.format))) return c->no_dispatch_return_value ();
|
||||
TRACE_DISPATCH (this, u.format);
|
||||
switch (u.format) {
|
||||
case 1: return_trace (c->dispatch (u.format1, std::forward<Ts> (ds)...));
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 2: return_trace (c->dispatch (u.format2, std::forward<Ts> (ds)...));
|
||||
#endif
|
||||
default:return_trace (c->default_return_value ());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_MARKLIGPOS_HH */
|
||||
@@ -0,0 +1,224 @@
|
||||
#ifndef OT_LAYOUT_GPOS_MARKLIGPOSFORMAT1_HH
|
||||
#define OT_LAYOUT_GPOS_MARKLIGPOSFORMAT1_HH
|
||||
|
||||
#include "LigatureArray.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
|
||||
template <typename Types>
|
||||
struct MarkLigPosFormat1_2
|
||||
{
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 1 */
|
||||
typename Types::template OffsetTo<Coverage>
|
||||
markCoverage; /* Offset to Mark Coverage table--from
|
||||
* beginning of MarkLigPos subtable */
|
||||
typename Types::template OffsetTo<Coverage>
|
||||
ligatureCoverage; /* Offset to Ligature Coverage
|
||||
* table--from beginning of MarkLigPos
|
||||
* subtable */
|
||||
HBUINT16 classCount; /* Number of defined mark classes */
|
||||
typename Types::template OffsetTo<MarkArray>
|
||||
markArray; /* Offset to MarkArray table--from
|
||||
* beginning of MarkLigPos subtable */
|
||||
typename Types::template OffsetTo<LigatureArray>
|
||||
ligatureArray; /* Offset to LigatureArray table--from
|
||||
* beginning of MarkLigPos subtable */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (4 + 4 * Types::size);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this) &&
|
||||
markCoverage.sanitize (c, this) &&
|
||||
ligatureCoverage.sanitize (c, this) &&
|
||||
markArray.sanitize (c, this) &&
|
||||
ligatureArray.sanitize (c, this, (unsigned int) classCount));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
return (this+markCoverage).intersects (glyphs) &&
|
||||
(this+ligatureCoverage).intersects (glyphs);
|
||||
}
|
||||
|
||||
void closure_lookups (hb_closure_lookups_context_t *c) const {}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
+ hb_zip (this+markCoverage, this+markArray)
|
||||
| hb_filter (c->glyph_set, hb_first)
|
||||
| hb_map (hb_second)
|
||||
| hb_apply ([&] (const MarkRecord& record) { record.collect_variation_indices (c, &(this+markArray)); })
|
||||
;
|
||||
|
||||
hb_map_t klass_mapping;
|
||||
Markclass_closure_and_remap_indexes (this+markCoverage, this+markArray, *c->glyph_set, &klass_mapping);
|
||||
|
||||
unsigned ligcount = (this+ligatureArray).len;
|
||||
auto lig_iter =
|
||||
+ hb_zip (this+ligatureCoverage, hb_range (ligcount))
|
||||
| hb_filter (c->glyph_set, hb_first)
|
||||
| hb_map (hb_second)
|
||||
;
|
||||
|
||||
const LigatureArray& lig_array = this+ligatureArray;
|
||||
for (const unsigned i : lig_iter)
|
||||
{
|
||||
hb_sorted_vector_t<unsigned> lig_indexes;
|
||||
unsigned row_count = lig_array[i].rows;
|
||||
for (unsigned row : + hb_range (row_count))
|
||||
{
|
||||
+ hb_range ((unsigned) classCount)
|
||||
| hb_filter (klass_mapping)
|
||||
| hb_map ([&] (const unsigned col) { return row * (unsigned) classCount + col; })
|
||||
| hb_sink (lig_indexes)
|
||||
;
|
||||
}
|
||||
|
||||
lig_array[i].collect_variation_indices (c, lig_indexes.iter ());
|
||||
}
|
||||
}
|
||||
|
||||
void collect_glyphs (hb_collect_glyphs_context_t *c) const
|
||||
{
|
||||
if (unlikely (!(this+markCoverage).collect_coverage (c->input))) return;
|
||||
if (unlikely (!(this+ligatureCoverage).collect_coverage (c->input))) return;
|
||||
}
|
||||
|
||||
const Coverage &get_coverage () const { return this+markCoverage; }
|
||||
|
||||
bool apply (hb_ot_apply_context_t *c) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
hb_buffer_t *buffer = c->buffer;
|
||||
unsigned int mark_index = (this+markCoverage).get_coverage (buffer->cur().codepoint);
|
||||
if (likely (mark_index == NOT_COVERED)) return_trace (false);
|
||||
|
||||
/* Now we search backwards for a non-mark glyph */
|
||||
|
||||
auto &skippy_iter = c->iter_input;
|
||||
skippy_iter.set_lookup_props (LookupFlag::IgnoreMarks);
|
||||
|
||||
if (c->last_base_until > buffer->idx)
|
||||
{
|
||||
c->last_base_until = 0;
|
||||
c->last_base = -1;
|
||||
}
|
||||
unsigned j;
|
||||
for (j = buffer->idx; j > c->last_base_until; j--)
|
||||
{
|
||||
auto match = skippy_iter.match (buffer->info[j - 1]);
|
||||
if (match == skippy_iter.MATCH)
|
||||
{
|
||||
c->last_base = (signed) j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
c->last_base_until = buffer->idx;
|
||||
if (c->last_base == -1)
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (0, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
unsigned idx = (unsigned) c->last_base;
|
||||
|
||||
/* Checking that matched glyph is actually a ligature by GDEF is too strong; disabled */
|
||||
//if (!_hb_glyph_info_is_ligature (&buffer->info[idx])) { return_trace (false); }
|
||||
|
||||
unsigned int lig_index = (this+ligatureCoverage).get_coverage (buffer->info[idx].codepoint);
|
||||
if (lig_index == NOT_COVERED)
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (idx, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
const LigatureArray& lig_array = this+ligatureArray;
|
||||
const LigatureAttach& lig_attach = lig_array[lig_index];
|
||||
|
||||
/* Find component to attach to */
|
||||
unsigned int comp_count = lig_attach.rows;
|
||||
if (unlikely (!comp_count))
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (idx, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
/* We must now check whether the ligature ID of the current mark glyph
|
||||
* is identical to the ligature ID of the found ligature. If yes, we
|
||||
* can directly use the component index. If not, we attach the mark
|
||||
* glyph to the last component of the ligature. */
|
||||
unsigned int comp_index;
|
||||
unsigned int lig_id = _hb_glyph_info_get_lig_id (&buffer->info[idx]);
|
||||
unsigned int mark_id = _hb_glyph_info_get_lig_id (&buffer->cur());
|
||||
unsigned int mark_comp = _hb_glyph_info_get_lig_comp (&buffer->cur());
|
||||
if (lig_id && lig_id == mark_id && mark_comp > 0)
|
||||
comp_index = hb_min (comp_count, _hb_glyph_info_get_lig_comp (&buffer->cur())) - 1;
|
||||
else
|
||||
comp_index = comp_count - 1;
|
||||
|
||||
return_trace ((this+markArray).apply (c, mark_index, comp_index, lig_attach, classCount, idx));
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
const hb_set_t &glyphset = *c->plan->glyphset_gsub ();
|
||||
const hb_map_t &glyph_map = c->plan->glyph_map_gsub;
|
||||
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
out->format = format;
|
||||
|
||||
hb_map_t klass_mapping;
|
||||
Markclass_closure_and_remap_indexes (this+markCoverage, this+markArray, glyphset, &klass_mapping);
|
||||
|
||||
if (!klass_mapping.get_population ()) return_trace (false);
|
||||
out->classCount = klass_mapping.get_population ();
|
||||
|
||||
auto mark_iter =
|
||||
+ hb_zip (this+markCoverage, this+markArray)
|
||||
| hb_filter (glyphset, hb_first)
|
||||
;
|
||||
|
||||
auto new_mark_coverage =
|
||||
+ mark_iter
|
||||
| hb_map_retains_sorting (hb_first)
|
||||
| hb_map_retains_sorting (glyph_map)
|
||||
;
|
||||
|
||||
if (!out->markCoverage.serialize_serialize (c->serializer, new_mark_coverage))
|
||||
return_trace (false);
|
||||
|
||||
if (unlikely (!out->markArray.serialize_subset (c, markArray, this,
|
||||
(this+markCoverage).iter (),
|
||||
&klass_mapping)))
|
||||
return_trace (false);
|
||||
|
||||
auto new_ligature_coverage =
|
||||
+ hb_iter (this + ligatureCoverage)
|
||||
| hb_take ((this + ligatureArray).len)
|
||||
| hb_map_retains_sorting (glyph_map)
|
||||
| hb_filter ([] (hb_codepoint_t glyph) { return glyph != HB_MAP_VALUE_INVALID; })
|
||||
;
|
||||
|
||||
if (!out->ligatureCoverage.serialize_serialize (c->serializer, new_ligature_coverage))
|
||||
return_trace (false);
|
||||
|
||||
return_trace (out->ligatureArray.serialize_subset (c, ligatureArray, this,
|
||||
hb_iter (this+ligatureCoverage),
|
||||
classCount, &klass_mapping));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_MARKLIGPOSFORMAT1_HH */
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef OT_LAYOUT_GPOS_MARKMARKPOS_HH
|
||||
#define OT_LAYOUT_GPOS_MARKMARKPOS_HH
|
||||
|
||||
#include "MarkMarkPosFormat1.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct MarkMarkPos
|
||||
{
|
||||
protected:
|
||||
union {
|
||||
HBUINT16 format; /* Format identifier */
|
||||
MarkMarkPosFormat1_2<SmallTypes> format1;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
MarkMarkPosFormat1_2<MediumTypes> format2;
|
||||
#endif
|
||||
} u;
|
||||
|
||||
public:
|
||||
template <typename context_t, typename ...Ts>
|
||||
typename context_t::return_t dispatch (context_t *c, Ts&&... ds) const
|
||||
{
|
||||
if (unlikely (!c->may_dispatch (this, &u.format))) return c->no_dispatch_return_value ();
|
||||
TRACE_DISPATCH (this, u.format);
|
||||
switch (u.format) {
|
||||
case 1: return_trace (c->dispatch (u.format1, std::forward<Ts> (ds)...));
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 2: return_trace (c->dispatch (u.format2, std::forward<Ts> (ds)...));
|
||||
#endif
|
||||
default:return_trace (c->default_return_value ());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_MARKMARKPOS_HH */
|
||||
@@ -0,0 +1,231 @@
|
||||
#ifndef OT_LAYOUT_GPOS_MARKMARKPOSFORMAT1_HH
|
||||
#define OT_LAYOUT_GPOS_MARKMARKPOSFORMAT1_HH
|
||||
|
||||
#include "MarkMarkPosFormat1.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
typedef AnchorMatrix Mark2Array; /* mark2-major--
|
||||
* in order of Mark2Coverage Index--,
|
||||
* mark1-minor--
|
||||
* ordered by class--zero-based. */
|
||||
|
||||
template <typename Types>
|
||||
struct MarkMarkPosFormat1_2
|
||||
{
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 1 */
|
||||
typename Types::template OffsetTo<Coverage>
|
||||
mark1Coverage; /* Offset to Combining Mark1 Coverage
|
||||
* table--from beginning of MarkMarkPos
|
||||
* subtable */
|
||||
typename Types::template OffsetTo<Coverage>
|
||||
mark2Coverage; /* Offset to Combining Mark2 Coverage
|
||||
* table--from beginning of MarkMarkPos
|
||||
* subtable */
|
||||
HBUINT16 classCount; /* Number of defined mark classes */
|
||||
typename Types::template OffsetTo<MarkArray>
|
||||
mark1Array; /* Offset to Mark1Array table--from
|
||||
* beginning of MarkMarkPos subtable */
|
||||
typename Types::template OffsetTo<Mark2Array>
|
||||
mark2Array; /* Offset to Mark2Array table--from
|
||||
* beginning of MarkMarkPos subtable */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (4 + 4 * Types::size);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this) &&
|
||||
mark1Coverage.sanitize (c, this) &&
|
||||
mark2Coverage.sanitize (c, this) &&
|
||||
mark1Array.sanitize (c, this) &&
|
||||
hb_barrier () &&
|
||||
mark2Array.sanitize (c, this, (unsigned int) classCount));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
return (this+mark1Coverage).intersects (glyphs) &&
|
||||
(this+mark2Coverage).intersects (glyphs);
|
||||
}
|
||||
|
||||
void closure_lookups (hb_closure_lookups_context_t *c) const {}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
+ hb_zip (this+mark1Coverage, this+mark1Array)
|
||||
| hb_filter (c->glyph_set, hb_first)
|
||||
| hb_map (hb_second)
|
||||
| hb_apply ([&] (const MarkRecord& record) { record.collect_variation_indices (c, &(this+mark1Array)); })
|
||||
;
|
||||
|
||||
hb_map_t klass_mapping;
|
||||
Markclass_closure_and_remap_indexes (this+mark1Coverage, this+mark1Array, *c->glyph_set, &klass_mapping);
|
||||
|
||||
unsigned mark2_count = (this+mark2Array).rows;
|
||||
auto mark2_iter =
|
||||
+ hb_zip (this+mark2Coverage, hb_range (mark2_count))
|
||||
| hb_filter (c->glyph_set, hb_first)
|
||||
| hb_map (hb_second)
|
||||
;
|
||||
|
||||
hb_sorted_vector_t<unsigned> mark2_indexes;
|
||||
for (const unsigned row : mark2_iter)
|
||||
{
|
||||
+ hb_range ((unsigned) classCount)
|
||||
| hb_filter (klass_mapping)
|
||||
| hb_map ([&] (const unsigned col) { return row * (unsigned) classCount + col; })
|
||||
| hb_sink (mark2_indexes)
|
||||
;
|
||||
}
|
||||
(this+mark2Array).collect_variation_indices (c, mark2_indexes.iter ());
|
||||
}
|
||||
|
||||
void collect_glyphs (hb_collect_glyphs_context_t *c) const
|
||||
{
|
||||
if (unlikely (!(this+mark1Coverage).collect_coverage (c->input))) return;
|
||||
if (unlikely (!(this+mark2Coverage).collect_coverage (c->input))) return;
|
||||
}
|
||||
|
||||
const Coverage &get_coverage () const { return this+mark1Coverage; }
|
||||
|
||||
bool apply (hb_ot_apply_context_t *c) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
hb_buffer_t *buffer = c->buffer;
|
||||
unsigned int mark1_index = (this+mark1Coverage).get_coverage (buffer->cur().codepoint);
|
||||
if (likely (mark1_index == NOT_COVERED)) return_trace (false);
|
||||
|
||||
/* now we search backwards for a suitable mark glyph until a non-mark glyph */
|
||||
auto &skippy_iter = c->iter_input;
|
||||
skippy_iter.reset_fast (buffer->idx);
|
||||
skippy_iter.set_lookup_props (c->lookup_props & ~(uint32_t)LookupFlag::IgnoreFlags);
|
||||
unsigned unsafe_from;
|
||||
if (unlikely (!skippy_iter.prev (&unsafe_from)))
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (unsafe_from, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
if (likely (!_hb_glyph_info_is_mark (&buffer->info[skippy_iter.idx])))
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (skippy_iter.idx, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
unsigned int j = skippy_iter.idx;
|
||||
|
||||
unsigned int id1 = _hb_glyph_info_get_lig_id (&buffer->cur());
|
||||
unsigned int id2 = _hb_glyph_info_get_lig_id (&buffer->info[j]);
|
||||
unsigned int comp1 = _hb_glyph_info_get_lig_comp (&buffer->cur());
|
||||
unsigned int comp2 = _hb_glyph_info_get_lig_comp (&buffer->info[j]);
|
||||
|
||||
if (likely (id1 == id2))
|
||||
{
|
||||
if (id1 == 0) /* Marks belonging to the same base. */
|
||||
goto good;
|
||||
else if (comp1 == comp2) /* Marks belonging to the same ligature component. */
|
||||
goto good;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If ligature ids don't match, it may be the case that one of the marks
|
||||
* itself is a ligature. In which case match. */
|
||||
if ((id1 > 0 && !comp1) || (id2 > 0 && !comp2))
|
||||
goto good;
|
||||
}
|
||||
|
||||
/* Didn't match. */
|
||||
buffer->unsafe_to_concat_from_outbuffer (skippy_iter.idx, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
|
||||
good:
|
||||
unsigned int mark2_index = (this+mark2Coverage).get_coverage (buffer->info[j].codepoint);
|
||||
if (mark2_index == NOT_COVERED)
|
||||
{
|
||||
buffer->unsafe_to_concat_from_outbuffer (skippy_iter.idx, buffer->idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
return_trace ((this+mark1Array).apply (c, mark1_index, mark2_index, this+mark2Array, classCount, j));
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
const hb_set_t &glyphset = *c->plan->glyphset_gsub ();
|
||||
const hb_map_t &glyph_map = *c->plan->glyph_map;
|
||||
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
out->format = format;
|
||||
|
||||
hb_map_t klass_mapping;
|
||||
Markclass_closure_and_remap_indexes (this+mark1Coverage, this+mark1Array, glyphset, &klass_mapping);
|
||||
|
||||
if (!klass_mapping.get_population ()) return_trace (false);
|
||||
out->classCount = klass_mapping.get_population ();
|
||||
|
||||
auto mark1_iter =
|
||||
+ hb_zip (this+mark1Coverage, this+mark1Array)
|
||||
| hb_filter (glyphset, hb_first)
|
||||
;
|
||||
|
||||
hb_sorted_vector_t<hb_codepoint_t> new_coverage;
|
||||
+ mark1_iter
|
||||
| hb_map (hb_first)
|
||||
| hb_map (glyph_map)
|
||||
| hb_sink (new_coverage)
|
||||
;
|
||||
|
||||
if (!out->mark1Coverage.serialize_serialize (c->serializer, new_coverage.iter ()))
|
||||
return_trace (false);
|
||||
|
||||
if (unlikely (!out->mark1Array.serialize_subset (c, mark1Array, this,
|
||||
(this+mark1Coverage).iter (),
|
||||
&klass_mapping)))
|
||||
return_trace (false);
|
||||
|
||||
unsigned mark2count = (this+mark2Array).rows;
|
||||
auto mark2_iter =
|
||||
+ hb_zip (this+mark2Coverage, hb_range (mark2count))
|
||||
| hb_filter (glyphset, hb_first)
|
||||
;
|
||||
|
||||
new_coverage.reset ();
|
||||
+ mark2_iter
|
||||
| hb_map (hb_first)
|
||||
| hb_map (glyph_map)
|
||||
| hb_sink (new_coverage)
|
||||
;
|
||||
|
||||
if (!out->mark2Coverage.serialize_serialize (c->serializer, new_coverage.iter ()))
|
||||
return_trace (false);
|
||||
|
||||
hb_sorted_vector_t<unsigned> mark2_indexes;
|
||||
for (const unsigned row : + mark2_iter
|
||||
| hb_map (hb_second))
|
||||
{
|
||||
+ hb_range ((unsigned) classCount)
|
||||
| hb_filter (klass_mapping)
|
||||
| hb_map ([&] (const unsigned col) { return row * (unsigned) classCount + col; })
|
||||
| hb_sink (mark2_indexes)
|
||||
;
|
||||
}
|
||||
|
||||
return_trace (out->mark2Array.serialize_subset (c, mark2Array, this,
|
||||
mark2_iter.len (),
|
||||
mark2_indexes.iter ()));
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_MARKMARKPOSFORMAT1_HH */
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef OT_LAYOUT_GPOS_MARKRECORD_HH
|
||||
#define OT_LAYOUT_GPOS_MARKRECORD_HH
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct MarkRecord
|
||||
{
|
||||
friend struct MarkArray;
|
||||
|
||||
public:
|
||||
HBUINT16 klass; /* Class defined for this mark */
|
||||
Offset16To<Anchor>
|
||||
markAnchor; /* Offset to Anchor table--from
|
||||
* beginning of MarkArray table */
|
||||
public:
|
||||
DEFINE_SIZE_STATIC (4);
|
||||
|
||||
unsigned get_class () const { return (unsigned) klass; }
|
||||
bool sanitize (hb_sanitize_context_t *c, const void *base) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
return_trace (c->check_struct (this) && markAnchor.sanitize (c, base));
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c,
|
||||
const void *src_base,
|
||||
const hb_map_t *klass_mapping) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
auto *out = c->serializer->embed (this);
|
||||
if (unlikely (!out)) return_trace (false);
|
||||
|
||||
out->klass = klass_mapping->get (klass);
|
||||
return_trace (out->markAnchor.serialize_subset (c, markAnchor, src_base));
|
||||
}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c,
|
||||
const void *src_base) const
|
||||
{
|
||||
(src_base+markAnchor).collect_variation_indices (c);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_MARKRECORD_HH */
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
#ifndef OT_LAYOUT_GPOS_PAIRPOS_HH
|
||||
#define OT_LAYOUT_GPOS_PAIRPOS_HH
|
||||
|
||||
#include "PairPosFormat1.hh"
|
||||
#include "PairPosFormat2.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct PairPos
|
||||
{
|
||||
protected:
|
||||
union {
|
||||
HBUINT16 format; /* Format identifier */
|
||||
PairPosFormat1_3<SmallTypes> format1;
|
||||
PairPosFormat2_4<SmallTypes> format2;
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
PairPosFormat1_3<MediumTypes> format3;
|
||||
PairPosFormat2_4<MediumTypes> format4;
|
||||
#endif
|
||||
} u;
|
||||
|
||||
public:
|
||||
template <typename context_t, typename ...Ts>
|
||||
typename context_t::return_t dispatch (context_t *c, Ts&&... ds) const
|
||||
{
|
||||
if (unlikely (!c->may_dispatch (this, &u.format))) return c->no_dispatch_return_value ();
|
||||
TRACE_DISPATCH (this, u.format);
|
||||
switch (u.format) {
|
||||
case 1: return_trace (c->dispatch (u.format1, std::forward<Ts> (ds)...));
|
||||
case 2: return_trace (c->dispatch (u.format2, std::forward<Ts> (ds)...));
|
||||
#ifndef HB_NO_BEYOND_64K
|
||||
case 3: return_trace (c->dispatch (u.format3, std::forward<Ts> (ds)...));
|
||||
case 4: return_trace (c->dispatch (u.format4, std::forward<Ts> (ds)...));
|
||||
#endif
|
||||
default:return_trace (c->default_return_value ());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_PAIRPOS_HH
|
||||
@@ -0,0 +1,271 @@
|
||||
#ifndef OT_LAYOUT_GPOS_PAIRPOSFORMAT1_HH
|
||||
#define OT_LAYOUT_GPOS_PAIRPOSFORMAT1_HH
|
||||
|
||||
#include "PairSet.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
|
||||
template <typename Types>
|
||||
struct PairPosFormat1_3
|
||||
{
|
||||
using PairSet = GPOS_impl::PairSet<Types>;
|
||||
using PairValueRecord = GPOS_impl::PairValueRecord<Types>;
|
||||
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 1 */
|
||||
typename Types::template OffsetTo<Coverage>
|
||||
coverage; /* Offset to Coverage table--from
|
||||
* beginning of subtable */
|
||||
ValueFormat valueFormat[2]; /* [0] Defines the types of data in
|
||||
* ValueRecord1--for the first glyph
|
||||
* in the pair--may be zero (0) */
|
||||
/* [1] Defines the types of data in
|
||||
* ValueRecord2--for the second glyph
|
||||
* in the pair--may be zero (0) */
|
||||
Array16Of<typename Types::template OffsetTo<PairSet>>
|
||||
pairSet; /* Array of PairSet tables
|
||||
* ordered by Coverage Index */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (8 + Types::size, pairSet);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
|
||||
if (!c->check_struct (this)) return_trace (false);
|
||||
hb_barrier ();
|
||||
|
||||
unsigned int len1 = valueFormat[0].get_len ();
|
||||
unsigned int len2 = valueFormat[1].get_len ();
|
||||
typename PairSet::sanitize_closure_t closure =
|
||||
{
|
||||
valueFormat,
|
||||
len1,
|
||||
PairSet::get_size (len1, len2)
|
||||
};
|
||||
|
||||
return_trace (coverage.sanitize (c, this) && pairSet.sanitize (c, this, &closure));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
auto &cov = this+coverage;
|
||||
|
||||
if (pairSet.len > glyphs->get_population () * hb_bit_storage ((unsigned) pairSet.len))
|
||||
{
|
||||
for (hb_codepoint_t g : glyphs->iter())
|
||||
{
|
||||
unsigned i = cov.get_coverage (g);
|
||||
if ((this+pairSet[i]).intersects (glyphs, valueFormat))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return
|
||||
+ hb_zip (cov, pairSet)
|
||||
| hb_filter (*glyphs, hb_first)
|
||||
| hb_map (hb_second)
|
||||
| hb_map ([glyphs, this] (const typename Types::template OffsetTo<PairSet> &_)
|
||||
{ return (this+_).intersects (glyphs, valueFormat); })
|
||||
| hb_any
|
||||
;
|
||||
}
|
||||
|
||||
void closure_lookups (hb_closure_lookups_context_t *c) const {}
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
if ((!valueFormat[0].has_device ()) && (!valueFormat[1].has_device ())) return;
|
||||
|
||||
auto it =
|
||||
+ hb_zip (this+coverage, pairSet)
|
||||
| hb_filter (c->glyph_set, hb_first)
|
||||
| hb_map (hb_second)
|
||||
;
|
||||
|
||||
if (!it) return;
|
||||
+ it
|
||||
| hb_map (hb_add (this))
|
||||
| hb_apply ([&] (const PairSet& _) { _.collect_variation_indices (c, valueFormat); })
|
||||
;
|
||||
}
|
||||
|
||||
void collect_glyphs (hb_collect_glyphs_context_t *c) const
|
||||
{
|
||||
if (unlikely (!(this+coverage).collect_coverage (c->input))) return;
|
||||
unsigned int count = pairSet.len;
|
||||
for (unsigned int i = 0; i < count; i++)
|
||||
(this+pairSet[i]).collect_glyphs (c, valueFormat);
|
||||
}
|
||||
|
||||
const Coverage &get_coverage () const { return this+coverage; }
|
||||
|
||||
unsigned cache_cost () const
|
||||
{
|
||||
return (this+coverage).cost ();
|
||||
}
|
||||
static void * cache_func (void *p, hb_ot_lookup_cache_op_t op)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case hb_ot_lookup_cache_op_t::CREATE:
|
||||
{
|
||||
hb_ot_lookup_cache_t *cache = (hb_ot_lookup_cache_t *) hb_malloc (sizeof (hb_ot_lookup_cache_t));
|
||||
if (likely (cache))
|
||||
cache->clear ();
|
||||
return cache;
|
||||
}
|
||||
case hb_ot_lookup_cache_op_t::ENTER:
|
||||
return (void *) true;
|
||||
case hb_ot_lookup_cache_op_t::LEAVE:
|
||||
return nullptr;
|
||||
case hb_ot_lookup_cache_op_t::DESTROY:
|
||||
{
|
||||
hb_ot_lookup_cache_t *cache = (hb_ot_lookup_cache_t *) p;
|
||||
hb_free (cache);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool apply_cached (hb_ot_apply_context_t *c) const { return _apply (c, true); }
|
||||
bool apply (hb_ot_apply_context_t *c) const { return _apply (c, false); }
|
||||
bool _apply (hb_ot_apply_context_t *c, bool cached) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
|
||||
hb_buffer_t *buffer = c->buffer;
|
||||
|
||||
#ifndef HB_NO_OT_LAYOUT_LOOKUP_CACHE
|
||||
hb_ot_lookup_cache_t *cache = cached ? (hb_ot_lookup_cache_t *) c->lookup_accel->cache : nullptr;
|
||||
unsigned int index = (this+coverage).get_coverage (buffer->cur().codepoint, cache);
|
||||
#else
|
||||
unsigned int index = (this+coverage).get_coverage (buffer->cur().codepoint);
|
||||
#endif
|
||||
if (index == NOT_COVERED) return_trace (false);
|
||||
|
||||
auto &skippy_iter = c->iter_input;
|
||||
skippy_iter.reset_fast (buffer->idx);
|
||||
unsigned unsafe_to;
|
||||
if (unlikely (!skippy_iter.next (&unsafe_to)))
|
||||
{
|
||||
buffer->unsafe_to_concat (buffer->idx, unsafe_to);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
return_trace ((this+pairSet[index]).apply (c, valueFormat, skippy_iter.idx));
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
|
||||
const hb_set_t &glyphset = *c->plan->glyphset_gsub ();
|
||||
const hb_map_t &glyph_map = *c->plan->glyph_map;
|
||||
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
out->format = format;
|
||||
|
||||
hb_pair_t<unsigned, unsigned> newFormats = hb_pair (valueFormat[0], valueFormat[1]);
|
||||
|
||||
if (c->plan->normalized_coords)
|
||||
{
|
||||
/* all device flags will be dropped when full instancing, no need to strip
|
||||
* hints, also do not strip emtpy cause we don't compute the new default
|
||||
* value during stripping */
|
||||
newFormats = compute_effective_value_formats (glyphset, false, false, &c->plan->layout_variation_idx_delta_map);
|
||||
}
|
||||
/* do not strip hints for VF */
|
||||
else if (c->plan->flags & HB_SUBSET_FLAGS_NO_HINTING)
|
||||
{
|
||||
hb_blob_t* blob = hb_face_reference_table (c->plan->source, HB_TAG ('f','v','a','r'));
|
||||
bool has_fvar = (blob != hb_blob_get_empty ());
|
||||
hb_blob_destroy (blob);
|
||||
|
||||
bool strip = !has_fvar;
|
||||
/* special case: strip hints when a VF has no GDEF varstore after
|
||||
* subsetting*/
|
||||
if (has_fvar && !c->plan->has_gdef_varstore)
|
||||
strip = true;
|
||||
newFormats = compute_effective_value_formats (glyphset, strip, true);
|
||||
}
|
||||
|
||||
out->valueFormat[0] = newFormats.first;
|
||||
out->valueFormat[1] = newFormats.second;
|
||||
|
||||
hb_sorted_vector_t<hb_codepoint_t> new_coverage;
|
||||
|
||||
+ hb_zip (this+coverage, pairSet)
|
||||
| hb_filter (glyphset, hb_first)
|
||||
| hb_filter ([this, c, out] (const typename Types::template OffsetTo<PairSet>& _)
|
||||
{
|
||||
auto snap = c->serializer->snapshot ();
|
||||
auto *o = out->pairSet.serialize_append (c->serializer);
|
||||
if (unlikely (!o)) return false;
|
||||
bool ret = o->serialize_subset (c, _, this, valueFormat, out->valueFormat);
|
||||
if (!ret)
|
||||
{
|
||||
out->pairSet.pop ();
|
||||
c->serializer->revert (snap);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
hb_second)
|
||||
| hb_map (hb_first)
|
||||
| hb_map (glyph_map)
|
||||
| hb_sink (new_coverage)
|
||||
;
|
||||
|
||||
out->coverage.serialize_serialize (c->serializer, new_coverage.iter ());
|
||||
|
||||
return_trace (bool (new_coverage));
|
||||
}
|
||||
|
||||
|
||||
hb_pair_t<unsigned, unsigned> compute_effective_value_formats (const hb_set_t& glyphset,
|
||||
bool strip_hints, bool strip_empty,
|
||||
const hb_hashmap_t<unsigned, hb_pair_t<unsigned, int>> *varidx_delta_map = nullptr) const
|
||||
{
|
||||
unsigned record_size = PairSet::get_size (valueFormat);
|
||||
|
||||
unsigned format1 = 0;
|
||||
unsigned format2 = 0;
|
||||
for (const auto & _ :
|
||||
+ hb_zip (this+coverage, pairSet)
|
||||
| hb_filter (glyphset, hb_first)
|
||||
| hb_map (hb_second)
|
||||
)
|
||||
{
|
||||
const PairSet& set = (this + _);
|
||||
const PairValueRecord *record = &set.firstPairValueRecord;
|
||||
|
||||
unsigned count = set.len;
|
||||
for (unsigned i = 0; i < count; i++)
|
||||
{
|
||||
if (record->intersects (glyphset))
|
||||
{
|
||||
format1 = format1 | valueFormat[0].get_effective_format (record->get_values_1 (), strip_hints, strip_empty, &set, varidx_delta_map);
|
||||
format2 = format2 | valueFormat[1].get_effective_format (record->get_values_2 (valueFormat[0]), strip_hints, strip_empty, &set, varidx_delta_map);
|
||||
}
|
||||
record = &StructAtOffset<const PairValueRecord> (record, record_size);
|
||||
}
|
||||
|
||||
if (format1 == valueFormat[0] && format2 == valueFormat[1])
|
||||
break;
|
||||
}
|
||||
|
||||
return hb_pair (format1, format2);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_PAIRPOSFORMAT1_HH
|
||||
@@ -0,0 +1,413 @@
|
||||
#ifndef OT_LAYOUT_GPOS_PAIRPOSFORMAT2_HH
|
||||
#define OT_LAYOUT_GPOS_PAIRPOSFORMAT2_HH
|
||||
|
||||
#include "ValueFormat.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
template <typename Types>
|
||||
struct PairPosFormat2_4 : ValueBase
|
||||
{
|
||||
protected:
|
||||
HBUINT16 format; /* Format identifier--format = 2 */
|
||||
typename Types::template OffsetTo<Coverage>
|
||||
coverage; /* Offset to Coverage table--from
|
||||
* beginning of subtable */
|
||||
ValueFormat valueFormat1; /* ValueRecord definition--for the
|
||||
* first glyph of the pair--may be zero
|
||||
* (0) */
|
||||
ValueFormat valueFormat2; /* ValueRecord definition--for the
|
||||
* second glyph of the pair--may be
|
||||
* zero (0) */
|
||||
typename Types::template OffsetTo<ClassDef>
|
||||
classDef1; /* Offset to ClassDef table--from
|
||||
* beginning of PairPos subtable--for
|
||||
* the first glyph of the pair */
|
||||
typename Types::template OffsetTo<ClassDef>
|
||||
classDef2; /* Offset to ClassDef table--from
|
||||
* beginning of PairPos subtable--for
|
||||
* the second glyph of the pair */
|
||||
HBUINT16 class1Count; /* Number of classes in ClassDef1
|
||||
* table--includes Class0 */
|
||||
HBUINT16 class2Count; /* Number of classes in ClassDef2
|
||||
* table--includes Class0 */
|
||||
ValueRecord values; /* Matrix of value pairs:
|
||||
* class1-major, class2-minor,
|
||||
* Each entry has value1 and value2 */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (10 + 3 * Types::size, values);
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
if (!(c->check_struct (this)
|
||||
&& coverage.sanitize (c, this)
|
||||
&& classDef1.sanitize (c, this)
|
||||
&& classDef2.sanitize (c, this))) return_trace (false);
|
||||
|
||||
unsigned int len1 = valueFormat1.get_len ();
|
||||
unsigned int len2 = valueFormat2.get_len ();
|
||||
unsigned int stride = HBUINT16::static_size * (len1 + len2);
|
||||
unsigned int count = (unsigned int) class1Count * (unsigned int) class2Count;
|
||||
return_trace (c->check_range ((const void *) values,
|
||||
count,
|
||||
stride) &&
|
||||
(c->lazy_some_gpos ||
|
||||
(valueFormat1.sanitize_values_stride_unsafe (c, this, &values[0], count, stride) &&
|
||||
valueFormat2.sanitize_values_stride_unsafe (c, this, &values[len1], count, stride))));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
return (this+coverage).intersects (glyphs) &&
|
||||
(this+classDef2).intersects (glyphs);
|
||||
}
|
||||
|
||||
void closure_lookups (hb_closure_lookups_context_t *c) const {}
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c) const
|
||||
{
|
||||
if (!intersects (c->glyph_set)) return;
|
||||
if ((!valueFormat1.has_device ()) && (!valueFormat2.has_device ())) return;
|
||||
|
||||
hb_set_t klass1_glyphs, klass2_glyphs;
|
||||
if (!(this+classDef1).collect_coverage (&klass1_glyphs)) return;
|
||||
if (!(this+classDef2).collect_coverage (&klass2_glyphs)) return;
|
||||
|
||||
hb_set_t class1_set, class2_set;
|
||||
for (const unsigned cp : + c->glyph_set->iter () | hb_filter (this + coverage))
|
||||
{
|
||||
if (!klass1_glyphs.has (cp)) class1_set.add (0);
|
||||
else
|
||||
{
|
||||
unsigned klass1 = (this+classDef1).get (cp);
|
||||
class1_set.add (klass1);
|
||||
}
|
||||
}
|
||||
|
||||
class2_set.add (0);
|
||||
for (const unsigned cp : + c->glyph_set->iter () | hb_filter (klass2_glyphs))
|
||||
{
|
||||
unsigned klass2 = (this+classDef2).get (cp);
|
||||
class2_set.add (klass2);
|
||||
}
|
||||
|
||||
if (class1_set.is_empty ()
|
||||
|| class2_set.is_empty ()
|
||||
|| (class2_set.get_population() == 1 && class2_set.has(0)))
|
||||
return;
|
||||
|
||||
unsigned len1 = valueFormat1.get_len ();
|
||||
unsigned len2 = valueFormat2.get_len ();
|
||||
const hb_array_t<const Value> values_array = values.as_array ((unsigned)class1Count * (unsigned) class2Count * (len1 + len2));
|
||||
for (const unsigned class1_idx : class1_set.iter ())
|
||||
{
|
||||
for (const unsigned class2_idx : class2_set.iter ())
|
||||
{
|
||||
unsigned start_offset = (class1_idx * (unsigned) class2Count + class2_idx) * (len1 + len2);
|
||||
if (valueFormat1.has_device ())
|
||||
valueFormat1.collect_variation_indices (c, this, values_array.sub_array (start_offset, len1));
|
||||
|
||||
if (valueFormat2.has_device ())
|
||||
valueFormat2.collect_variation_indices (c, this, values_array.sub_array (start_offset+len1, len2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collect_glyphs (hb_collect_glyphs_context_t *c) const
|
||||
{
|
||||
if (unlikely (!(this+coverage).collect_coverage (c->input))) return;
|
||||
if (unlikely (!(this+classDef2).collect_coverage (c->input))) return;
|
||||
}
|
||||
|
||||
const Coverage &get_coverage () const { return this+coverage; }
|
||||
|
||||
struct pair_pos_cache_t
|
||||
{
|
||||
hb_ot_lookup_cache_t coverage;
|
||||
hb_ot_lookup_cache_t first;
|
||||
hb_ot_lookup_cache_t second;
|
||||
};
|
||||
|
||||
unsigned cache_cost () const
|
||||
{
|
||||
return (this+coverage).cost () + (this+classDef1).cost () + (this+classDef2).cost ();
|
||||
}
|
||||
static void * cache_func (void *p, hb_ot_lookup_cache_op_t op)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case hb_ot_lookup_cache_op_t::CREATE:
|
||||
{
|
||||
pair_pos_cache_t *cache = (pair_pos_cache_t *) hb_malloc (sizeof (pair_pos_cache_t));
|
||||
if (likely (cache))
|
||||
{
|
||||
cache->coverage.clear ();
|
||||
cache->first.clear ();
|
||||
cache->second.clear ();
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
case hb_ot_lookup_cache_op_t::ENTER:
|
||||
return (void *) true;
|
||||
case hb_ot_lookup_cache_op_t::LEAVE:
|
||||
return nullptr;
|
||||
case hb_ot_lookup_cache_op_t::DESTROY:
|
||||
{
|
||||
pair_pos_cache_t *cache = (pair_pos_cache_t *) p;
|
||||
hb_free (cache);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool apply_cached (hb_ot_apply_context_t *c) const { return _apply (c, true); }
|
||||
bool apply (hb_ot_apply_context_t *c) const { return _apply (c, false); }
|
||||
bool _apply (hb_ot_apply_context_t *c, bool cached) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
|
||||
hb_buffer_t *buffer = c->buffer;
|
||||
|
||||
#ifndef HB_NO_OT_LAYOUT_LOOKUP_CACHE
|
||||
pair_pos_cache_t *cache = cached ? (pair_pos_cache_t *) c->lookup_accel->cache : nullptr;
|
||||
unsigned int index = (this+coverage).get_coverage (buffer->cur().codepoint, cache ? &cache->coverage : nullptr);
|
||||
#else
|
||||
unsigned int index = (this+coverage).get_coverage (buffer->cur().codepoint);
|
||||
#endif
|
||||
if (index == NOT_COVERED) return_trace (false);
|
||||
|
||||
auto &skippy_iter = c->iter_input;
|
||||
skippy_iter.reset_fast (buffer->idx);
|
||||
unsigned unsafe_to;
|
||||
if (unlikely (!skippy_iter.next (&unsafe_to)))
|
||||
{
|
||||
buffer->unsafe_to_concat (buffer->idx, unsafe_to);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
#ifndef HB_NO_OT_LAYOUT_LOOKUP_CACHE
|
||||
unsigned int klass1 = (this+classDef1).get_class (buffer->cur().codepoint, cache ? &cache->first : nullptr);
|
||||
unsigned int klass2 = (this+classDef2).get_class (buffer->info[skippy_iter.idx].codepoint, cache ? &cache->second : nullptr);
|
||||
#else
|
||||
unsigned int klass1 = (this+classDef1).get_class (buffer->cur().codepoint);
|
||||
unsigned int klass2 = (this+classDef2).get_class (buffer->info[skippy_iter.idx].codepoint);
|
||||
#endif
|
||||
if (unlikely (klass1 >= class1Count || klass2 >= class2Count))
|
||||
{
|
||||
buffer->unsafe_to_concat (buffer->idx, skippy_iter.idx + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
unsigned int len1 = valueFormat1.get_len ();
|
||||
unsigned int len2 = valueFormat2.get_len ();
|
||||
unsigned int record_len = len1 + len2;
|
||||
|
||||
const Value *v = &values[record_len * (klass1 * class2Count + klass2)];
|
||||
|
||||
bool applied_first = false, applied_second = false;
|
||||
|
||||
|
||||
/* Isolate simple kerning and apply it half to each side.
|
||||
* Results in better cursor positioning / underline drawing.
|
||||
*
|
||||
* Disabled, because causes issues... :-(
|
||||
* https://github.com/harfbuzz/harfbuzz/issues/3408
|
||||
* https://github.com/harfbuzz/harfbuzz/pull/3235#issuecomment-1029814978
|
||||
*/
|
||||
#ifndef HB_SPLIT_KERN
|
||||
if (false)
|
||||
#endif
|
||||
{
|
||||
if (!len2)
|
||||
{
|
||||
const hb_direction_t dir = buffer->props.direction;
|
||||
const bool horizontal = HB_DIRECTION_IS_HORIZONTAL (dir);
|
||||
const bool backward = HB_DIRECTION_IS_BACKWARD (dir);
|
||||
unsigned mask = horizontal ? ValueFormat::xAdvance : ValueFormat::yAdvance;
|
||||
if (backward)
|
||||
mask |= mask >> 2; /* Add eg. xPlacement in RTL. */
|
||||
/* Add Devices. */
|
||||
mask |= mask << 4;
|
||||
|
||||
if (valueFormat1 & ~mask)
|
||||
goto bail;
|
||||
|
||||
/* Is simple kern. Apply value on an empty position slot,
|
||||
* then split it between sides. */
|
||||
|
||||
hb_glyph_position_t pos{};
|
||||
if (valueFormat1.apply_value (c, this, v, pos))
|
||||
{
|
||||
hb_position_t *src = &pos.x_advance;
|
||||
hb_position_t *dst1 = &buffer->cur_pos().x_advance;
|
||||
hb_position_t *dst2 = &buffer->pos[skippy_iter.idx].x_advance;
|
||||
unsigned i = horizontal ? 0 : 1;
|
||||
|
||||
hb_position_t kern = src[i];
|
||||
hb_position_t kern1 = kern >> 1;
|
||||
hb_position_t kern2 = kern - kern1;
|
||||
|
||||
if (!backward)
|
||||
{
|
||||
dst1[i] += kern1;
|
||||
dst2[i] += kern2;
|
||||
dst2[i + 2] += kern2;
|
||||
}
|
||||
else
|
||||
{
|
||||
dst1[i] += kern1;
|
||||
dst1[i + 2] += src[i + 2] - kern2;
|
||||
dst2[i] += kern2;
|
||||
}
|
||||
|
||||
applied_first = applied_second = kern != 0;
|
||||
goto success;
|
||||
}
|
||||
goto boring;
|
||||
}
|
||||
}
|
||||
bail:
|
||||
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"try kerning glyphs at %u,%u",
|
||||
c->buffer->idx, skippy_iter.idx);
|
||||
}
|
||||
|
||||
applied_first = len1 && valueFormat1.apply_value (c, this, v, buffer->cur_pos());
|
||||
applied_second = len2 && valueFormat2.apply_value (c, this, v + len1, buffer->pos[skippy_iter.idx]);
|
||||
|
||||
if (applied_first || applied_second)
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"kerned glyphs at %u,%u",
|
||||
c->buffer->idx, skippy_iter.idx);
|
||||
}
|
||||
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"tried kerning glyphs at %u,%u",
|
||||
c->buffer->idx, skippy_iter.idx);
|
||||
}
|
||||
|
||||
success:
|
||||
if (applied_first || applied_second)
|
||||
buffer->unsafe_to_break (buffer->idx, skippy_iter.idx + 1);
|
||||
else
|
||||
boring:
|
||||
buffer->unsafe_to_concat (buffer->idx, skippy_iter.idx + 1);
|
||||
|
||||
if (len2)
|
||||
{
|
||||
skippy_iter.idx++;
|
||||
// https://github.com/harfbuzz/harfbuzz/issues/3824
|
||||
// https://github.com/harfbuzz/harfbuzz/issues/3888#issuecomment-1326781116
|
||||
buffer->unsafe_to_break (buffer->idx, skippy_iter.idx + 1);
|
||||
}
|
||||
|
||||
buffer->idx = skippy_iter.idx;
|
||||
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
out->format = format;
|
||||
|
||||
hb_map_t klass1_map;
|
||||
out->classDef1.serialize_subset (c, classDef1, this, &klass1_map, true, true, &(this + coverage));
|
||||
out->class1Count = klass1_map.get_population ();
|
||||
|
||||
hb_map_t klass2_map;
|
||||
out->classDef2.serialize_subset (c, classDef2, this, &klass2_map, true, false);
|
||||
out->class2Count = klass2_map.get_population ();
|
||||
|
||||
unsigned len1 = valueFormat1.get_len ();
|
||||
unsigned len2 = valueFormat2.get_len ();
|
||||
|
||||
hb_pair_t<unsigned, unsigned> newFormats = hb_pair (valueFormat1, valueFormat2);
|
||||
|
||||
if (c->plan->normalized_coords)
|
||||
{
|
||||
/* in case of full instancing, all var device flags will be dropped so no
|
||||
* need to strip hints here */
|
||||
newFormats = compute_effective_value_formats (klass1_map, klass2_map, false, false, &c->plan->layout_variation_idx_delta_map);
|
||||
}
|
||||
/* do not strip hints for VF */
|
||||
else if (c->plan->flags & HB_SUBSET_FLAGS_NO_HINTING)
|
||||
{
|
||||
hb_blob_t* blob = hb_face_reference_table (c->plan->source, HB_TAG ('f','v','a','r'));
|
||||
bool has_fvar = (blob != hb_blob_get_empty ());
|
||||
hb_blob_destroy (blob);
|
||||
|
||||
bool strip = !has_fvar;
|
||||
/* special case: strip hints when a VF has no GDEF varstore after
|
||||
* subsetting*/
|
||||
if (has_fvar && !c->plan->has_gdef_varstore)
|
||||
strip = true;
|
||||
newFormats = compute_effective_value_formats (klass1_map, klass2_map, strip, true);
|
||||
}
|
||||
|
||||
out->valueFormat1 = newFormats.first;
|
||||
out->valueFormat2 = newFormats.second;
|
||||
|
||||
unsigned total_len = len1 + len2;
|
||||
hb_vector_t<unsigned> class2_idxs (+ hb_range ((unsigned) class2Count) | hb_filter (klass2_map));
|
||||
for (unsigned class1_idx : + hb_range ((unsigned) class1Count) | hb_filter (klass1_map))
|
||||
{
|
||||
for (unsigned class2_idx : class2_idxs)
|
||||
{
|
||||
unsigned idx = (class1_idx * (unsigned) class2Count + class2_idx) * total_len;
|
||||
valueFormat1.copy_values (c->serializer, out->valueFormat1, this, &values[idx], &c->plan->layout_variation_idx_delta_map);
|
||||
valueFormat2.copy_values (c->serializer, out->valueFormat2, this, &values[idx + len1], &c->plan->layout_variation_idx_delta_map);
|
||||
}
|
||||
}
|
||||
|
||||
bool ret = out->coverage.serialize_subset(c, coverage, this);
|
||||
return_trace (out->class1Count && out->class2Count && ret);
|
||||
}
|
||||
|
||||
|
||||
hb_pair_t<unsigned, unsigned> compute_effective_value_formats (const hb_map_t& klass1_map,
|
||||
const hb_map_t& klass2_map,
|
||||
bool strip_hints, bool strip_empty,
|
||||
const hb_hashmap_t<unsigned, hb_pair_t<unsigned, int>> *varidx_delta_map = nullptr) const
|
||||
{
|
||||
unsigned len1 = valueFormat1.get_len ();
|
||||
unsigned len2 = valueFormat2.get_len ();
|
||||
unsigned record_size = len1 + len2;
|
||||
|
||||
unsigned format1 = 0;
|
||||
unsigned format2 = 0;
|
||||
|
||||
for (unsigned class1_idx : + hb_range ((unsigned) class1Count) | hb_filter (klass1_map))
|
||||
{
|
||||
for (unsigned class2_idx : + hb_range ((unsigned) class2Count) | hb_filter (klass2_map))
|
||||
{
|
||||
unsigned idx = (class1_idx * (unsigned) class2Count + class2_idx) * record_size;
|
||||
format1 = format1 | valueFormat1.get_effective_format (&values[idx], strip_hints, strip_empty, this, varidx_delta_map);
|
||||
format2 = format2 | valueFormat2.get_effective_format (&values[idx + len1], strip_hints, strip_empty, this, varidx_delta_map);
|
||||
}
|
||||
|
||||
if (format1 == valueFormat1 && format2 == valueFormat2)
|
||||
break;
|
||||
}
|
||||
|
||||
return hb_pair (format1, format2);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_PAIRPOSFORMAT2_HH
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
#ifndef OT_LAYOUT_GPOS_PAIRSET_HH
|
||||
#define OT_LAYOUT_GPOS_PAIRSET_HH
|
||||
|
||||
#include "PairValueRecord.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
|
||||
template <typename Types>
|
||||
struct PairSet : ValueBase
|
||||
{
|
||||
template <typename Types2>
|
||||
friend struct PairPosFormat1_3;
|
||||
|
||||
using PairValueRecord = GPOS_impl::PairValueRecord<Types>;
|
||||
|
||||
protected:
|
||||
HBUINT16 len; /* Number of PairValueRecords */
|
||||
PairValueRecord firstPairValueRecord;
|
||||
/* Array of PairValueRecords--ordered
|
||||
* by GlyphID of the second glyph */
|
||||
public:
|
||||
DEFINE_SIZE_MIN (2);
|
||||
|
||||
static unsigned get_size (unsigned len1, unsigned len2)
|
||||
{
|
||||
return Types::HBGlyphID::static_size + Value::static_size * (len1 + len2);
|
||||
}
|
||||
static unsigned get_size (const ValueFormat valueFormats[2])
|
||||
{
|
||||
unsigned len1 = valueFormats[0].get_len ();
|
||||
unsigned len2 = valueFormats[1].get_len ();
|
||||
return get_size (len1, len2);
|
||||
}
|
||||
|
||||
struct sanitize_closure_t
|
||||
{
|
||||
const ValueFormat *valueFormats;
|
||||
unsigned int len1; /* valueFormats[0].get_len() */
|
||||
unsigned int stride; /* bytes */
|
||||
};
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c, const sanitize_closure_t *closure) const
|
||||
{
|
||||
TRACE_SANITIZE (this);
|
||||
if (!(c->check_struct (this) &&
|
||||
hb_barrier () &&
|
||||
c->check_range (&firstPairValueRecord,
|
||||
len,
|
||||
closure->stride))) return_trace (false);
|
||||
hb_barrier ();
|
||||
|
||||
unsigned int count = len;
|
||||
const PairValueRecord *record = &firstPairValueRecord;
|
||||
return_trace (c->lazy_some_gpos ||
|
||||
(closure->valueFormats[0].sanitize_values_stride_unsafe (c, this, &record->values[0], count, closure->stride) &&
|
||||
closure->valueFormats[1].sanitize_values_stride_unsafe (c, this, &record->values[closure->len1], count, closure->stride)));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs,
|
||||
const ValueFormat *valueFormats) const
|
||||
{
|
||||
unsigned record_size = get_size (valueFormats);
|
||||
|
||||
const PairValueRecord *record = &firstPairValueRecord;
|
||||
unsigned int count = len;
|
||||
for (unsigned int i = 0; i < count; i++)
|
||||
{
|
||||
if (glyphs->has (record->secondGlyph))
|
||||
return true;
|
||||
record = &StructAtOffset<const PairValueRecord> (record, record_size);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void collect_glyphs (hb_collect_glyphs_context_t *c,
|
||||
const ValueFormat *valueFormats) const
|
||||
{
|
||||
unsigned record_size = get_size (valueFormats);
|
||||
|
||||
const PairValueRecord *record = &firstPairValueRecord;
|
||||
c->input->add_array (&record->secondGlyph, len, record_size);
|
||||
}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c,
|
||||
const ValueFormat *valueFormats) const
|
||||
{
|
||||
unsigned record_size = get_size (valueFormats);
|
||||
|
||||
const PairValueRecord *record = &firstPairValueRecord;
|
||||
unsigned count = len;
|
||||
for (unsigned i = 0; i < count; i++)
|
||||
{
|
||||
if (c->glyph_set->has (record->secondGlyph))
|
||||
{ record->collect_variation_indices (c, valueFormats, this); }
|
||||
|
||||
record = &StructAtOffset<const PairValueRecord> (record, record_size);
|
||||
}
|
||||
}
|
||||
|
||||
bool apply (hb_ot_apply_context_t *c,
|
||||
const ValueFormat *valueFormats,
|
||||
unsigned int pos) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
hb_buffer_t *buffer = c->buffer;
|
||||
unsigned int len1 = valueFormats[0].get_len ();
|
||||
unsigned int len2 = valueFormats[1].get_len ();
|
||||
unsigned record_size = get_size (len1, len2);
|
||||
|
||||
const PairValueRecord *record = hb_bsearch (buffer->info[pos].codepoint,
|
||||
&firstPairValueRecord,
|
||||
len,
|
||||
record_size);
|
||||
if (record)
|
||||
{
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"try kerning glyphs at %u,%u",
|
||||
c->buffer->idx, pos);
|
||||
}
|
||||
|
||||
bool applied_first = len1 && valueFormats[0].apply_value (c, this, &record->values[0], buffer->cur_pos());
|
||||
bool applied_second = len2 && valueFormats[1].apply_value (c, this, &record->values[len1], buffer->pos[pos]);
|
||||
|
||||
if (applied_first || applied_second)
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"kerned glyphs at %u,%u",
|
||||
c->buffer->idx, pos);
|
||||
}
|
||||
|
||||
if (HB_BUFFER_MESSAGE_MORE && c->buffer->messaging ())
|
||||
{
|
||||
c->buffer->message (c->font,
|
||||
"tried kerning glyphs at %u,%u",
|
||||
c->buffer->idx, pos);
|
||||
}
|
||||
|
||||
if (applied_first || applied_second)
|
||||
buffer->unsafe_to_break (buffer->idx, pos + 1);
|
||||
|
||||
if (len2)
|
||||
{
|
||||
pos++;
|
||||
// https://github.com/harfbuzz/harfbuzz/issues/3824
|
||||
// https://github.com/harfbuzz/harfbuzz/issues/3888#issuecomment-1326781116
|
||||
buffer->unsafe_to_break (buffer->idx, pos + 1);
|
||||
}
|
||||
|
||||
buffer->idx = pos;
|
||||
return_trace (true);
|
||||
}
|
||||
buffer->unsafe_to_concat (buffer->idx, pos + 1);
|
||||
return_trace (false);
|
||||
}
|
||||
|
||||
bool subset (hb_subset_context_t *c,
|
||||
const ValueFormat valueFormats[2],
|
||||
const ValueFormat newFormats[2]) const
|
||||
{
|
||||
TRACE_SUBSET (this);
|
||||
auto snap = c->serializer->snapshot ();
|
||||
|
||||
auto *out = c->serializer->start_embed (*this);
|
||||
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
||||
out->len = 0;
|
||||
|
||||
const hb_set_t &glyphset = *c->plan->glyphset_gsub ();
|
||||
const hb_map_t &glyph_map = *c->plan->glyph_map;
|
||||
|
||||
unsigned len1 = valueFormats[0].get_len ();
|
||||
unsigned len2 = valueFormats[1].get_len ();
|
||||
unsigned record_size = get_size (len1, len2);
|
||||
|
||||
typename PairValueRecord::context_t context =
|
||||
{
|
||||
this,
|
||||
valueFormats,
|
||||
newFormats,
|
||||
len1,
|
||||
&glyph_map,
|
||||
&c->plan->layout_variation_idx_delta_map
|
||||
};
|
||||
|
||||
const PairValueRecord *record = &firstPairValueRecord;
|
||||
unsigned count = len, num = 0;
|
||||
for (unsigned i = 0; i < count; i++)
|
||||
{
|
||||
if (glyphset.has (record->secondGlyph)
|
||||
&& record->subset (c, &context)) num++;
|
||||
record = &StructAtOffset<const PairValueRecord> (record, record_size);
|
||||
}
|
||||
|
||||
out->len = num;
|
||||
if (!num) c->serializer->revert (snap);
|
||||
return_trace (num);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_PAIRSET_HH
|
||||
@@ -0,0 +1,99 @@
|
||||
#ifndef OT_LAYOUT_GPOS_PAIRVALUERECORD_HH
|
||||
#define OT_LAYOUT_GPOS_PAIRVALUERECORD_HH
|
||||
|
||||
#include "ValueFormat.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
|
||||
template <typename Types>
|
||||
struct PairValueRecord
|
||||
{
|
||||
template <typename Types2>
|
||||
friend struct PairSet;
|
||||
|
||||
protected:
|
||||
typename Types::HBGlyphID
|
||||
secondGlyph; /* GlyphID of second glyph in the
|
||||
* pair--first glyph is listed in the
|
||||
* Coverage table */
|
||||
ValueRecord values; /* Positioning data for the first glyph
|
||||
* followed by for second glyph */
|
||||
public:
|
||||
DEFINE_SIZE_ARRAY (Types::HBGlyphID::static_size, values);
|
||||
|
||||
int cmp (hb_codepoint_t k) const
|
||||
{ return secondGlyph.cmp (k); }
|
||||
|
||||
struct context_t
|
||||
{
|
||||
const ValueBase *base;
|
||||
const ValueFormat *valueFormats;
|
||||
const ValueFormat *newFormats;
|
||||
unsigned len1; /* valueFormats[0].get_len() */
|
||||
const hb_map_t *glyph_map;
|
||||
const hb_hashmap_t<unsigned, hb_pair_t<unsigned, int>> *layout_variation_idx_delta_map;
|
||||
};
|
||||
|
||||
bool subset (hb_subset_context_t *c,
|
||||
context_t *closure) const
|
||||
{
|
||||
TRACE_SERIALIZE (this);
|
||||
auto *s = c->serializer;
|
||||
auto *out = s->start_embed (*this);
|
||||
if (unlikely (!s->extend_min (out))) return_trace (false);
|
||||
|
||||
out->secondGlyph = (*closure->glyph_map)[secondGlyph];
|
||||
|
||||
closure->valueFormats[0].copy_values (s,
|
||||
closure->newFormats[0],
|
||||
closure->base, &values[0],
|
||||
closure->layout_variation_idx_delta_map);
|
||||
closure->valueFormats[1].copy_values (s,
|
||||
closure->newFormats[1],
|
||||
closure->base,
|
||||
&values[closure->len1],
|
||||
closure->layout_variation_idx_delta_map);
|
||||
|
||||
return_trace (true);
|
||||
}
|
||||
|
||||
void collect_variation_indices (hb_collect_variation_indices_context_t *c,
|
||||
const ValueFormat *valueFormats,
|
||||
const ValueBase *base) const
|
||||
{
|
||||
unsigned record1_len = valueFormats[0].get_len ();
|
||||
unsigned record2_len = valueFormats[1].get_len ();
|
||||
const hb_array_t<const Value> values_array = values.as_array (record1_len + record2_len);
|
||||
|
||||
if (valueFormats[0].has_device ())
|
||||
valueFormats[0].collect_variation_indices (c, base, values_array.sub_array (0, record1_len));
|
||||
|
||||
if (valueFormats[1].has_device ())
|
||||
valueFormats[1].collect_variation_indices (c, base, values_array.sub_array (record1_len, record2_len));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t& glyphset) const
|
||||
{
|
||||
return glyphset.has(secondGlyph);
|
||||
}
|
||||
|
||||
const Value* get_values_1 () const
|
||||
{
|
||||
return &values[0];
|
||||
}
|
||||
|
||||
const Value* get_values_2 (ValueFormat format1) const
|
||||
{
|
||||
return &values[format1.get_len ()];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OT_LAYOUT_GPOS_PAIRVALUERECORD_HH
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
#ifndef OT_LAYOUT_GPOS_POSLOOKUP_HH
|
||||
#define OT_LAYOUT_GPOS_POSLOOKUP_HH
|
||||
|
||||
#include "PosLookupSubTable.hh"
|
||||
#include "../../../hb-ot-layout-common.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct PosLookup : Lookup
|
||||
{
|
||||
using SubTable = PosLookupSubTable;
|
||||
|
||||
const SubTable& get_subtable (unsigned int i) const
|
||||
{ return Lookup::get_subtable<SubTable> (i); }
|
||||
|
||||
bool is_reverse () const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool apply (hb_ot_apply_context_t *c) const
|
||||
{
|
||||
TRACE_APPLY (this);
|
||||
return_trace (dispatch (c));
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs) const
|
||||
{
|
||||
hb_intersects_context_t c (glyphs);
|
||||
return dispatch (&c);
|
||||
}
|
||||
|
||||
hb_collect_glyphs_context_t::return_t collect_glyphs (hb_collect_glyphs_context_t *c) const
|
||||
{ return dispatch (c); }
|
||||
|
||||
hb_closure_lookups_context_t::return_t closure_lookups (hb_closure_lookups_context_t *c, unsigned this_index) const
|
||||
{
|
||||
if (c->is_lookup_visited (this_index))
|
||||
return hb_closure_lookups_context_t::default_return_value ();
|
||||
|
||||
c->set_lookup_visited (this_index);
|
||||
if (!intersects (c->glyphs))
|
||||
{
|
||||
c->set_lookup_inactive (this_index);
|
||||
return hb_closure_lookups_context_t::default_return_value ();
|
||||
}
|
||||
|
||||
hb_closure_lookups_context_t::return_t ret = dispatch (c);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <typename set_t>
|
||||
void collect_coverage (set_t *glyphs) const
|
||||
{
|
||||
hb_collect_coverage_context_t<set_t> c (glyphs);
|
||||
dispatch (&c);
|
||||
}
|
||||
|
||||
template <typename context_t>
|
||||
static typename context_t::return_t dispatch_recurse_func (context_t *c, unsigned int lookup_index);
|
||||
|
||||
template <typename context_t, typename ...Ts>
|
||||
typename context_t::return_t dispatch (context_t *c, Ts&&... ds) const
|
||||
{ return Lookup::dispatch<SubTable> (c, std::forward<Ts> (ds)...); }
|
||||
|
||||
bool subset (hb_subset_context_t *c) const
|
||||
{ return Lookup::subset<SubTable> (c); }
|
||||
|
||||
bool sanitize (hb_sanitize_context_t *c) const
|
||||
{ return Lookup::sanitize<SubTable> (c); }
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_POSLOOKUP_HH */
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef OT_LAYOUT_GPOS_POSLOOKUPSUBTABLE_HH
|
||||
#define OT_LAYOUT_GPOS_POSLOOKUPSUBTABLE_HH
|
||||
|
||||
#include "SinglePos.hh"
|
||||
#include "PairPos.hh"
|
||||
#include "CursivePos.hh"
|
||||
#include "MarkBasePos.hh"
|
||||
#include "MarkLigPos.hh"
|
||||
#include "MarkMarkPos.hh"
|
||||
#include "ContextPos.hh"
|
||||
#include "ChainContextPos.hh"
|
||||
#include "ExtensionPos.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct PosLookupSubTable
|
||||
{
|
||||
friend struct ::OT::Lookup;
|
||||
friend struct PosLookup;
|
||||
|
||||
enum Type {
|
||||
Single = 1,
|
||||
Pair = 2,
|
||||
Cursive = 3,
|
||||
MarkBase = 4,
|
||||
MarkLig = 5,
|
||||
MarkMark = 6,
|
||||
Context = 7,
|
||||
ChainContext = 8,
|
||||
Extension = 9
|
||||
};
|
||||
|
||||
template <typename context_t, typename ...Ts>
|
||||
typename context_t::return_t dispatch (context_t *c, unsigned int lookup_type, Ts&&... ds) const
|
||||
{
|
||||
TRACE_DISPATCH (this, lookup_type);
|
||||
switch (lookup_type) {
|
||||
case Single: return_trace (u.single.dispatch (c, std::forward<Ts> (ds)...));
|
||||
case Pair: return_trace (u.pair.dispatch (c, std::forward<Ts> (ds)...));
|
||||
case Cursive: return_trace (u.cursive.dispatch (c, std::forward<Ts> (ds)...));
|
||||
case MarkBase: return_trace (u.markBase.dispatch (c, std::forward<Ts> (ds)...));
|
||||
case MarkLig: return_trace (u.markLig.dispatch (c, std::forward<Ts> (ds)...));
|
||||
case MarkMark: return_trace (u.markMark.dispatch (c, std::forward<Ts> (ds)...));
|
||||
case Context: return_trace (u.context.dispatch (c, std::forward<Ts> (ds)...));
|
||||
case ChainContext: return_trace (u.chainContext.dispatch (c, std::forward<Ts> (ds)...));
|
||||
case Extension: return_trace (u.extension.dispatch (c, std::forward<Ts> (ds)...));
|
||||
default: return_trace (c->default_return_value ());
|
||||
}
|
||||
}
|
||||
|
||||
bool intersects (const hb_set_t *glyphs, unsigned int lookup_type) const
|
||||
{
|
||||
hb_intersects_context_t c (glyphs);
|
||||
return dispatch (&c, lookup_type);
|
||||
}
|
||||
|
||||
protected:
|
||||
union {
|
||||
SinglePos single;
|
||||
PairPos pair;
|
||||
CursivePos cursive;
|
||||
MarkBasePos markBase;
|
||||
MarkLigPos markLig;
|
||||
MarkMarkPos markMark;
|
||||
ContextPos context;
|
||||
ChainContextPos chainContext;
|
||||
ExtensionPos extension;
|
||||
} u;
|
||||
public:
|
||||
DEFINE_SIZE_MIN (0);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* HB_OT_LAYOUT_GPOS_POSLOOKUPSUBTABLE_HH */
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
#ifndef OT_LAYOUT_GPOS_SINGLEPOS_HH
|
||||
#define OT_LAYOUT_GPOS_SINGLEPOS_HH
|
||||
|
||||
#include "SinglePosFormat1.hh"
|
||||
#include "SinglePosFormat2.hh"
|
||||
|
||||
namespace OT {
|
||||
namespace Layout {
|
||||
namespace GPOS_impl {
|
||||
|
||||
struct SinglePos
|
||||
{
|
||||
protected:
|
||||
union {
|
||||
HBUINT16 format; /* Format identifier */
|
||||
SinglePosFormat1 format1;
|
||||
SinglePosFormat2 format2;
|
||||
} u;
|
||||
|
||||
public:
|
||||
template<typename Iterator,
|
||||
hb_requires (hb_is_iterator (Iterator))>
|
||||
unsigned get_format (Iterator glyph_val_iter_pairs)
|
||||
{
|
||||
hb_array_t<const Value> first_val_iter = hb_second (*glyph_val_iter_pairs);
|
||||
|
||||
for (const auto iter : glyph_val_iter_pairs)
|
||||
for (const auto _ : hb_zip (iter.second, first_val_iter))
|
||||
if (_.first != _.second)
|
||||
return 2;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
template<typename Iterator,
|
||||
typename SrcLookup,
|
||||
hb_requires (hb_is_iterator (Iterator))>
|
||||
void serialize (hb_serialize_context_t *c,
|
||||
const SrcLookup* src,
|
||||
Iterator glyph_val_iter_pairs,
|
||||
const hb_hashmap_t<unsigned, hb_pair_t<unsigned, int>> *layout_variation_idx_delta_map,
|
||||
unsigned newFormat)
|
||||
{
|
||||
if (unlikely (!c->extend_min (u.format))) return;
|
||||
unsigned format = 2;
|
||||
ValueFormat new_format;
|
||||
new_format = newFormat;
|
||||
|
||||
if (glyph_val_iter_pairs)
|
||||
format = get_format (glyph_val_iter_pairs);
|
||||
|
||||
u.format = format;
|
||||
switch (u.format) {
|
||||
case 1: u.format1.serialize (c,
|
||||
src,
|
||||
glyph_val_iter_pairs,
|
||||
new_format,
|
||||
layout_variation_idx_delta_map);
|
||||
return;
|
||||
case 2: u.format2.serialize (c,
|
||||
src,
|
||||
glyph_val_iter_pairs,
|
||||
new_format,
|
||||
layout_variation_idx_delta_map);
|
||||
return;
|
||||
default:return;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename context_t, typename ...Ts>
|
||||
typename context_t::return_t dispatch (context_t *c, Ts&&... ds) const
|
||||
{
|
||||
if (unlikely (!c->may_dispatch (this, &u.format))) return c->no_dispatch_return_value ();
|
||||
TRACE_DISPATCH (this, u.format);
|
||||
switch (u.format) {
|
||||
case 1: return_trace (c->dispatch (u.format1, std::forward<Ts> (ds)...));
|
||||
case 2: return_trace (c->dispatch (u.format2, std::forward<Ts> (ds)...));
|
||||
default:return_trace (c->default_return_value ());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
template<typename Iterator, typename SrcLookup>
|
||||
static void
|
||||
SinglePos_serialize (hb_serialize_context_t *c,
|
||||
const SrcLookup *src,
|
||||
Iterator it,
|
||||
const hb_hashmap_t<unsigned, hb_pair_t<unsigned, int>> *layout_variation_idx_delta_map,
|
||||
unsigned new_format)
|
||||
{ c->start_embed<SinglePos> ()->serialize (c, src, it, layout_variation_idx_delta_map, new_format); }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OT_LAYOUT_GPOS_SINGLEPOS_HH */
|
||||
Loaded 100 of 607 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user