git.iko.soy/rowtype-yoga/purescript-tauri
on main help sign in
Add native QuickJS host, Skia Canvas, and stdin handoff
commit6a5ceb62377fbf004c34926e07ab3f65624f0a4a authorMark <mark.eibes@gmail.com> dateThu 1 Oct 2026, 09:37 (1 week ago) parent434fa4b
23 files changed+3435−7
native/launch-file/build.rs+1−1 native/launch-file/src/lib.rs+23−2 native/runtime/Cargo.toml+23 native/runtime/licenses/etc1.txt+164 native/runtime/licenses/expat.txt+23 native/runtime/licenses/fframes.txt+25 native/runtime/licenses/jpeg-ijg.txt+262 native/runtime/licenses/libjpeg-turbo.txt+140 native/runtime/licenses/libpng.txt+137 native/runtime/licenses/rust-skia.txt+25 native/runtime/licenses/skia.txt+32 native/runtime/licenses/wuffs.txt+205 native/runtime/licenses/zlib.txt+22 native/runtime/src/graphics.rs+892 native/runtime/src/lib.rs+604 native/runtime/src/process.rs+331 native/runtime/src/terminal.rs+281 src/Tauri/Launch.js+2 src/Tauri/Launch.purs+11−4 src/Tauri/Native.js+29 src/Tauri/Native.purs+71 src/Tauri/Native/Canvas.js+25 src/Tauri/Native/Canvas.purs+107
native/launch-file/build.rs+1−1
@@ -1,3 +1,3 @@
11fn main() {
2− tauri_plugin::Builder::new(&["arguments", "authorize_file_argument"]).build();
2+ tauri_plugin::Builder::new(&["arguments", "authorize_file_argument", "read_stdin"]).build();
33}
native/launch-file/src/lib.rs+23−2
@@ -2,7 +2,11 @@
22//! Register alongside tauri-plugin-fs; the frontend still needs the relevant fs
33//! command permissions. Argument interpretation belongs to the application.
44
5−use std::{env, fs, io, path::PathBuf};
5+use std::{
6+ env, fs,
7+ io::{self, Read},
8+ path::PathBuf,
9+};
610use tauri::{
711 plugin::{Builder, TauriPlugin},
812 AppHandle, Manager, Runtime, State,
@@ -38,6 +42,19 @@ fn arguments(launch: State<'_, Launch>) -> Vec<String> {
3842 launch.arguments.clone()
3943}
4044
45+#[tauri::command]
46+async fn read_stdin() -> Result<String, String> {
47+ tauri::async_runtime::spawn_blocking(|| {
48+ let mut source = String::new();
49+ io::stdin()
50+ .read_to_string(&mut source)
51+ .map_err(|error| format!("Cannot read standard input: {error}"))?;
52+ Ok(source)
53+ })
54+ .await
55+ .map_err(|error| format!("Standard input worker failed: {error}"))?
56+}
57+
4158#[tauri::command]
4259fn authorize_file_argument<R: Runtime>(
4360 app: AppHandle<R>,
@@ -61,7 +78,11 @@ fn authorize_file_argument<R: Runtime>(
6178
6279pub fn init<R: Runtime>() -> TauriPlugin<R> {
6380 Builder::new("launch-file")
64− .invoke_handler(tauri::generate_handler![arguments, authorize_file_argument])
81+ .invoke_handler(tauri::generate_handler![
82+ arguments,
83+ authorize_file_argument,
84+ read_stdin
85+ ])
6586 .setup(|app, _| {
6687 let cwd = env::current_dir()?;
6788 let arguments = env::args_os()
native/runtime/Cargo.tomlnew file+23
@@ -0,0 +1,23 @@
1+[package]
2+name = "tauri-native-host"
3+version = "0.0.1"
4+edition = "2021"
5+license = "MIT"
6+description = "Embedded PureScript JavaScript host with native graphics and terminal primitives"
7+
8+[dependencies]
9+rquickjs = "0.9.0"
10+# Keep upstream's default feature set for its CPU prebuilt binaries. No SVG,
11+# textlayout or GPU feature is enabled; text is shaped by Rustybuzz below.
12+skia-safe = "=0.153.3"
13+fontdb = "0.23.0"
14+rustybuzz = "0.20.1"
15+svgtypes = "0.15.3"
16+unicode-segmentation = "1.12"
17+unicode-script = "0.5"
18+crossterm = "0.28.1"
19+ctrlc = { version = "3.4", features = ["termination"] }
20+
21+[target.'cfg(unix)'.dependencies]
22+libc = "0.2"
23+
native/runtime/licenses/etc1.txtnew file+164
@@ -0,0 +1,164 @@
1+Skia m153-0.101.2 embedded ETC1 codec.
2+Source: https://raw.githubusercontent.com/rust-skia/skia/m153-0.101.2/third_party/etc1/LICENSE
3+
4+Apache License
5+
6+Version 2.0, January 2004
7+
8+http://www.apache.org/licenses/
9+
10+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
11+
12+1. Definitions.
13+
14+"License" shall mean the terms and conditions for use, reproduction, and
15+distribution as defined by Sections 1 through 9 of this document.
16+
17+"Licensor" shall mean the copyright owner or entity authorized by the
18+copyright owner that is granting the License.
19+
20+"Legal Entity" shall mean the union of the acting entity and all other
21+entities that control, are controlled by, or are under common control with
22+that entity. For the purposes of this definition, "control" means (i) the
23+power, direct or indirect, to cause the direction or management of such
24+entity, whether by contract or otherwise, or (ii) ownership of fifty
25+percent (50%) or more of the outstanding shares, or (iii) beneficial
26+ownership of such entity.
27+
28+"You" (or "Your") shall mean an individual or Legal Entity exercising
29+permissions granted by this License.
30+
31+"Source" form shall mean the preferred form for making modifications,
32+including but not limited to software source code, documentation
33+source, and configuration files.
34+
35+"Object" form shall mean any form resulting from mechanical transformation
36+or translation of a Source form, including but not limited to compiled
37+object code, generated documentation, and conversions to other media types.
38+
39+"Work" shall mean the work of authorship, whether in Source or Object
40+form, made available under the License, as indicated by a copyright
41+notice that is included in or attached to the work (an example is
42+provided in the Appendix below).
43+
44+"Derivative Works" shall mean any work, whether in Source or Object
45+form, that is based on (or derived from) the Work and for which the
46+editorial revisions, annotations, elaborations, or other modifications
47+represent, as a whole, an original work of authorship. For the purposes
48+of this License, Derivative Works shall not include works that remain
49+separable from, or merely link (or bind by name) to the interfaces of,
50+the Work and Derivative Works thereof.
51+
52+"Contribution" shall mean any work of authorship, including the original
53+version of the Work and any modifications or additions to that Work or
54+Derivative Works thereof, that is intentionally submitted to Licensor
55+for inclusion in the Work by the copyright owner or by an individual or
56+Legal Entity authorized to submit on behalf of the copyright owner. For
57+the purposes of this definition, "submitted" means any form of electronic,
58+verbal, or written communication sent to the Licensor or its
59+representatives, including but not limited to communication on electronic
60+mailing lists, source code control systems, and issue tracking systems that
61+are managed by, or on behalf of, the Licensor for the purpose of discussing
62+and improving the Work, but excluding communication that is conspicuously
63+marked or otherwise designated in writing by the copyright owner as "Not
64+a Contribution."
65+
66+"Contributor" shall mean Licensor and any individual or Legal Entity on
67+behalf of whom a Contribution has been received by Licensor and subsequently
68+incorporated within the Work.
69+
70+2. Grant of Copyright License. Subject to the terms and conditions of this
71+License, each Contributor hereby grants to You a perpetual, worldwide,
72+non-exclusive, no-charge, royalty-free, irrevocable copyright license to
73+reproduce, prepare Derivative Works of, publicly display, publicly perform,
74+sublicense, and distribute the Work and such Derivative Works in Source or
75+Object form.
76+
77+3. Grant of Patent License. Subject to the terms and conditions of this
78+License, each Contributor hereby grants to You a perpetual, worldwide,
79+non-exclusive, no-charge, royalty-free, irrevocable (except as stated in
80+this section) patent license to make, have made, use, offer to sell, sell,
81+import, and otherwise transfer the Work, where such license applies only to
82+those patent claims licensable by such Contributor that are necessarily
83+infringed by their Contribution(s) alone or by combination of their
84+Contribution(s) with the Work to which such Contribution(s) was submitted.
85+If You institute patent litigation against any entity (including a cross-claim
86+or counterclaim in a lawsuit) alleging that the Work or a Contribution
87+incorporated within the Work constitutes direct or contributory patent
88+infringement, then any patent licenses granted to You under this License
89+for that Work shall terminate as of the date such litigation is filed.
90+
91+4. Redistribution. You may reproduce and distribute copies of the Work or
92+Derivative Works thereof in any medium, with or without modifications, and
93+in Source or Object form, provided that You meet the following conditions:
94+
95+You must give any other recipients of the Work or Derivative Works a copy of
96+this License; and
97+You must cause any modified files to carry prominent notices stating that
98+You changed the files; and
99+You must retain, in the Source form of any Derivative Works that You
100+distribute, all copyright, patent, trademark, and attribution notices
101+from the Source form of the Work, excluding those notices that do not
102+pertain to any part of the Derivative Works; and
103+If the Work includes a "NOTICE" text file as part of its distribution,
104+then any Derivative Works that You distribute must include a readable
105+copy of the attribution notices contained within such NOTICE file, excluding
106+those notices that do not pertain to any part of the Derivative Works, in
107+at least one of the following places: within a NOTICE text file distributed
108+as part of the Derivative Works; within the Source form or documentation, if
109+provided along with the Derivative Works; or, within a display generated by
110+the Derivative Works, if and wherever such third-party notices normally
111+appear. The contents of the NOTICE file are for informational purposes
112+only and do not modify the License. You may add Your own attribution
113+notices within Derivative Works that You distribute, alongside or as
114+an addendum to the NOTICE text from the Work, provided that such additional
115+attribution notices cannot be construed as modifying the License.
116+
117+You may add Your own copyright statement to Your modifications and may provide
118+additional or different license terms and conditions for use, reproduction, or
119+distribution of Your modifications, or for any such Derivative Works as a
120+whole, provided Your use, reproduction, and distribution of the Work otherwise
121+complies with the conditions stated in this License.
122+5. Submission of Contributions. Unless You explicitly state otherwise, any
123+Contribution intentionally submitted for inclusion in the Work by You to the
124+Licensor shall be under the terms and conditions of this License, without any
125+additional terms or conditions. Notwithstanding the above, nothing herein
126+shall supersede or modify the terms of any separate license agreement you
127+may have executed with Licensor regarding such Contributions.
128+
129+6. Trademarks. This License does not grant permission to use the trade names,
130+trademarks, service marks, or product names of the Licensor, except as
131+required for reasonable and customary use in describing the origin of the
132+Work and reproducing the content of the NOTICE file.
133+
134+7. Disclaimer of Warranty. Unless required by applicable law or agreed to
135+in writing, Licensor provides the Work (and each Contributor provides its
136+Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
137+ANY KIND, either express or implied, including, without limitation, any
138+warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or
139+FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining
140+the appropriateness of using or redistributing the Work and assume any risks
141+associated with Your exercise of permissions under this License.
142+
143+8. Limitation of Liability. In no event and under no legal theory, whether in
144+tort (including negligence), contract, or otherwise, unless required by
145+applicable law (such as deliberate and grossly negligent acts) or agreed to
146+in writing, shall any Contributor be liable to You for damages, including
147+any direct, indirect, special, incidental, or consequential damages of any
148+character arising as a result of this License or out of the use or inability
149+to use the Work (including but not limited to damages for loss of goodwill,
150+work stoppage, computer failure or malfunction, or any and all other
151+commercial damages or losses), even if such Contributor has been advised
152+of the possibility of such damages.
153+
154+9. Accepting Warranty or Additional Liability. While redistributing the
155+Work or Derivative Works thereof, You may choose to offer, and charge a
156+fee for, acceptance of support, warranty, indemnity, or other liability
157+obligations and/or rights consistent with this License. However, in accepting
158+such obligations, You may act only on Your own behalf and on Your sole
159+responsibility, not on behalf of any other Contributor, and only if You
160+agree to indemnify, defend, and hold each Contributor harmless for any
161+liability incurred by, or claims asserted against, such Contributor by
162+reason of your accepting any such warranty or additional liability.
163+
164+END OF TERMS AND CONDITIONS
native/runtime/licenses/expat.txtnew file+23
@@ -0,0 +1,23 @@
1+Source: https://chromium.googlesource.com/external/github.com/libexpat/libexpat.git/+/6154446fccefbf3ca644894f598969113b0c7bcd/expat/COPYING?format=TEXT
2+
3+Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper
4+Copyright (c) 2001-2025 Expat maintainers
5+
6+Permission is hereby granted, free of charge, to any person obtaining
7+a copy of this software and associated documentation files (the
8+"Software"), to deal in the Software without restriction, including
9+without limitation the rights to use, copy, modify, merge, publish,
10+distribute, sublicense, and/or sell copies of the Software, and to
11+permit persons to whom the Software is furnished to do so, subject to
12+the following conditions:
13+
14+The above copyright notice and this permission notice shall be included
15+in all copies or substantial portions of the Software.
16+
17+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
native/runtime/licenses/fframes.txtnew file+25
@@ -0,0 +1,25 @@
1+fframes 4ffb052d3430e0672b4f3647450c181606ddf418
2+Adapted CPU raster surface creation, packed path construction, paint configuration and explicit RGBA readback patterns from fframes-skia-renderer/src/backends/mod.rs, render/convert.rs and frame_renderer.rs. No usvgr text code is included.
3+Source: https://raw.githubusercontent.com/dmtrKovalenko/fframes/4ffb052d3430e0672b4f3647450c181606ddf418/LICENSE.txt
4+
5+MIT License
6+
7+Copyright (c) 2025-2026 Dmitriy Kovalenko
8+
9+Permission is hereby granted, free of charge, to any person obtaining a copy
10+of this software and associated documentation files (the "Software"), to deal
11+in the Software without restriction, including without limitation the rights
12+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+copies of the Software, and to permit persons to whom the Software is
14+furnished to do so, subject to the following conditions:
15+
16+The above copyright notice and this permission notice shall be included in all
17+copies or substantial portions of the Software.
18+
19+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+SOFTWARE.
native/runtime/licenses/jpeg-ijg.txtnew file+262
@@ -0,0 +1,262 @@
1+Source: https://chromium.googlesource.com/chromium/deps/libjpeg_turbo.git/+/e14cbfaa85529d47f9f55b0f104a579c1061f9ad/README.ijg?format=TEXT
2+
3+libjpeg-turbo note: This file has been modified by The libjpeg-turbo Project
4+to include only information relevant to libjpeg-turbo, to wordsmith certain
5+sections, and to remove impolitic language that existed in the libjpeg v8
6+README. It is included only for reference. Please see README.md for
7+information specific to libjpeg-turbo.
8+
9+
10+The Independent JPEG Group's JPEG software
11+==========================================
12+
13+This distribution contains a release of the Independent JPEG Group's free JPEG
14+software. You are welcome to redistribute this software and to use it for any
15+purpose, subject to the conditions under LEGAL ISSUES, below.
16+
17+This software is the work of Tom Lane, Guido Vollbeding, Philip Gladstone,
18+Bill Allombert, Jim Boucher, Lee Crocker, Bob Friesenhahn, Ben Jackson,
19+Julian Minguillon, Luis Ortiz, George Phillips, Davide Rossi, Ge' Weijers,
20+and other members of the Independent JPEG Group.
21+
22+IJG is not affiliated with the ISO/IEC JTC1/SC29/WG1 standards committee
23+(also known as JPEG, together with ITU-T SG16).
24+
25+
26+DOCUMENTATION ROADMAP
27+=====================
28+
29+This file contains the following sections:
30+
31+OVERVIEW General description of JPEG and the IJG software.
32+LEGAL ISSUES Copyright, lack of warranty, terms of distribution.
33+REFERENCES Where to learn more about JPEG.
34+ARCHIVE LOCATIONS Where to find newer versions of this software.
35+FILE FORMAT WARS Software *not* to get.
36+TO DO Plans for future IJG releases.
37+
38+Other documentation files in the distribution are:
39+
40+User documentation:
41+ doc/usage.txt Usage instructions for cjpeg, djpeg, jpegtran,
42+ rdjpgcom, and wrjpgcom.
43+ doc/*.1 Unix-style man pages for programs (same info as
44+ usage.txt).
45+ doc/wizard.txt Advanced usage instructions for JPEG wizards only.
46+ doc/change.log Version-to-version change highlights.
47+Programmer and internal documentation:
48+ doc/libjpeg.txt How to use the JPEG library in your own programs.
49+ src/example.c Sample code for calling the JPEG library.
50+ doc/structure.txt Overview of the JPEG library's internal structure.
51+ doc/coderules.txt Coding style rules --- please read if you contribute
52+ code.
53+
54+Please read at least usage.txt. Some information can also be found in the JPEG
55+FAQ (Frequently Asked Questions) article. See ARCHIVE LOCATIONS below to find
56+out where to obtain the FAQ article.
57+
58+If you want to understand how the JPEG code works, we suggest reading one or
59+more of the REFERENCES, then looking at the documentation files (in roughly
60+the order listed) before diving into the code.
61+
62+
63+OVERVIEW
64+========
65+
66+This package contains C software to implement JPEG image encoding, decoding,
67+and transcoding. JPEG (pronounced "jay-peg") is a standardized compression
68+method for full-color and grayscale images. JPEG's strong suit is compressing
69+photographic images or other types of images that have smooth color and
70+brightness transitions between neighboring pixels. Images with sharp lines or
71+other abrupt features may not compress well with JPEG, and a higher JPEG
72+quality may have to be used to avoid visible compression artifacts with such
73+images.
74+
75+JPEG is normally lossy, meaning that the output pixels are not necessarily
76+identical to the input pixels. However, on photographic content and other
77+"smooth" images, very good compression ratios can be obtained with no visible
78+compression artifacts, and extremely high compression ratios are possible if
79+you are willing to sacrifice image quality (by reducing the "quality" setting
80+in the compressor.)
81+
82+This software implements JPEG baseline, extended-sequential, progressive, and
83+lossless compression processes. Provision is made for supporting all variants
84+of these processes, although some uncommon parameter settings aren't
85+implemented yet. We have made no provision for supporting the hierarchical
86+processes defined in the standard.
87+
88+We provide a set of library routines for reading and writing JPEG image files,
89+plus two sample applications "cjpeg" and "djpeg", which use the library to
90+perform conversion between JPEG and some other popular image file formats.
91+The library is intended to be reused in other applications.
92+
93+In order to support file conversion and viewing software, we have included
94+considerable functionality beyond the bare JPEG coding/decoding capability;
95+for example, the color quantization modules are not strictly part of JPEG
96+decoding, but they are essential for output to colormapped file formats. These
97+extra functions can be compiled out of the library if not required for a
98+particular application.
99+
100+We have also included "jpegtran", a utility for lossless transcoding between
101+different JPEG processes, and "rdjpgcom" and "wrjpgcom", two simple
102+applications for inserting and extracting textual comments in JFIF files.
103+
104+The emphasis in designing this software has been on achieving portability and
105+flexibility, while also making it fast enough to be useful. In particular,
106+the software is not intended to be read as a tutorial on JPEG. (See the
107+REFERENCES section for introductory material.) Rather, it is intended to
108+be reliable, portable, industrial-strength code. We do not claim to have
109+achieved that goal in every aspect of the software, but we strive for it.
110+
111+We welcome the use of this software as a component of commercial products.
112+No royalty is required, but we do ask for an acknowledgement in product
113+documentation, as described under LEGAL ISSUES.
114+
115+
116+LEGAL ISSUES
117+============
118+
119+In plain English:
120+
121+1. We don't promise that this software works. (But if you find any bugs,
122+ please let us know!)
123+2. You can use this software for whatever you want. You don't have to pay us.
124+3. You may not pretend that you wrote this software. If you use it in a
125+ program, you must acknowledge somewhere in your documentation that
126+ you've used the IJG code.
127+
128+In legalese:
129+
130+The authors make NO WARRANTY or representation, either express or implied,
131+with respect to this software, its quality, accuracy, merchantability, or
132+fitness for a particular purpose. This software is provided "AS IS", and you,
133+its user, assume the entire risk as to its quality and accuracy.
134+
135+This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding.
136+All Rights Reserved except as specified below.
137+
138+Permission is hereby granted to use, copy, modify, and distribute this
139+software (or portions thereof) for any purpose, without fee, subject to these
140+conditions:
141+(1) If any part of the source code for this software is distributed, then this
142+README file must be included, with this copyright and no-warranty notice
143+unaltered; and any additions, deletions, or changes to the original files
144+must be clearly indicated in accompanying documentation.
145+(2) If only executable code is distributed, then the accompanying
146+documentation must state that "this software is based in part on the work of
147+the Independent JPEG Group".
148+(3) Permission for use of this software is granted only if the user accepts
149+full responsibility for any undesirable consequences; the authors accept
150+NO LIABILITY for damages of any kind.
151+
152+These conditions apply to any software derived from or based on the IJG code,
153+not just to the unmodified library. If you use our work, you ought to
154+acknowledge us.
155+
156+Permission is NOT granted for the use of any IJG author's name or company name
157+in advertising or publicity relating to this software or products derived from
158+it. This software may be referred to only as "the Independent JPEG Group's
159+software".
160+
161+We specifically permit and encourage the use of this software as the basis of
162+commercial products, provided that all warranty or liability claims are
163+assumed by the product vendor.
164+
165+
166+REFERENCES
167+==========
168+
169+We recommend reading one or more of these references before trying to
170+understand the innards of the JPEG software.
171+
172+The best short technical introduction to the JPEG compression algorithm is
173+ Wallace, Gregory K. "The JPEG Still Picture Compression Standard",
174+ Communications of the ACM, April 1991 (vol. 34 no. 4), pp. 30-44.
175+(Adjacent articles in that issue discuss MPEG motion picture compression,
176+applications of JPEG, and related topics.) If you don't have the CACM issue
177+handy, a PDF file containing a revised version of Wallace's article is
178+available at http://www.ijg.org/files/Wallace.JPEG.pdf. The file (actually
179+a preprint for an article that appeared in IEEE Trans. Consumer Electronics)
180+omits the sample images that appeared in CACM, but it includes corrections
181+and some added material. Note: the Wallace article is copyright ACM and IEEE,
182+and it may not be used for commercial purposes.
183+
184+A somewhat less technical, more leisurely introduction to JPEG can be found in
185+"The Data Compression Book" by Mark Nelson and Jean-loup Gailly, published by
186+M&T Books (New York), 2nd ed. 1996, ISBN 1-55851-434-1. This book provides
187+good explanations and example C code for a multitude of compression methods
188+including JPEG. It is an excellent source if you are comfortable reading C
189+code but don't know much about data compression in general. The book's JPEG
190+sample code is far from industrial-strength, but when you are ready to look
191+at a full implementation, you've got one here...
192+
193+The best currently available description of JPEG is the textbook "JPEG Still
194+Image Data Compression Standard" by William B. Pennebaker and Joan L.
195+Mitchell, published by Van Nostrand Reinhold, 1993, ISBN 0-442-01272-1.
196+Price US$59.95, 638 pp. The book includes the complete text of the ISO JPEG
197+standards (DIS 10918-1 and draft DIS 10918-2).
198+
199+The original JPEG standard is divided into two parts, Part 1 being the actual
200+specification, while Part 2 covers compliance testing methods. Part 1 is
201+titled "Digital Compression and Coding of Continuous-tone Still Images,
202+Part 1: Requirements and guidelines" and has document numbers ISO/IEC IS
203+10918-1, ITU-T T.81. Part 2 is titled "Digital Compression and Coding of
204+Continuous-tone Still Images, Part 2: Compliance testing" and has document
205+numbers ISO/IEC IS 10918-2, ITU-T T.83.
206+
207+The JPEG standard does not specify all details of an interchangeable file
208+format. For the omitted details, we follow the "JFIF" conventions, revision
209+1.02. JFIF version 1 has been adopted as ISO/IEC 10918-5 (05/2013) and
210+Recommendation ITU-T T.871 (05/2011): Information technology - Digital
211+compression and coding of continuous-tone still images: JPEG File Interchange
212+Format (JFIF). It is available as a free download in PDF file format from
213+https://www.iso.org/standard/54989.html and http://www.itu.int/rec/T-REC-T.871.
214+A PDF file of the older JFIF 1.02 specification is available at
215+http://www.w3.org/Graphics/JPEG/jfif3.pdf.
216+
217+The TIFF 6.0 file format specification can be obtained from
218+http://mirrors.ctan.org/graphics/tiff/TIFF6.ps.gz. The JPEG incorporation
219+scheme found in the TIFF 6.0 spec of 3-June-92 has a number of serious
220+problems. IJG does not recommend use of the TIFF 6.0 design (TIFF Compression
221+tag 6). Instead, we recommend the JPEG design proposed by TIFF Technical Note
222+#2 (Compression tag 7). Copies of this Note can be obtained from
223+http://www.ijg.org/files/. It is expected that the next revision
224+of the TIFF spec will replace the 6.0 JPEG design with the Note's design.
225+Although IJG's own code does not support TIFF/JPEG, the free libtiff library
226+uses our library to implement TIFF/JPEG per the Note.
227+
228+
229+ARCHIVE LOCATIONS
230+=================
231+
232+The "official" archive site for this software is www.ijg.org.
233+The most recent released version can always be found there in
234+directory "files".
235+
236+The JPEG FAQ (Frequently Asked Questions) article is a source of some
237+general information about JPEG. It is available at
238+http://www.faqs.org/faqs/jpeg-faq.
239+
240+
241+FILE FORMAT COMPATIBILITY
242+=========================
243+
244+This software implements ITU T.81 | ISO/IEC 10918 with some extensions from
245+ITU T.871 | ISO/IEC 10918-5 (JPEG File Interchange Format-- see REFERENCES).
246+Informally, the term "JPEG image" or "JPEG file" most often refers to JFIF or
247+a subset thereof, but there are other formats containing the name "JPEG" that
248+are incompatible with the original JPEG standard or with JFIF (for instance,
249+JPEG 2000 and JPEG XR). This software therefore does not support these
250+formats. Indeed, one of the original reasons for developing this free software
251+was to help force convergence on a common, interoperable format standard for
252+JPEG files.
253+
254+JFIF is a minimal or "low end" representation. TIFF/JPEG (TIFF revision 6.0 as
255+modified by TIFF Technical Note #2) can be used for "high end" applications
256+that need to record a lot of additional data about an image.
257+
258+
259+TO DO
260+=====
261+
262+Please send bug reports, offers of help, etc. to jpeg-info@jpegclub.org.
native/runtime/licenses/libjpeg-turbo.txtnew file+140
@@ -0,0 +1,140 @@
1+libjpeg-turbo: pinned Skia m153-0.101.2 native dependency.
2+Source: https://chromium.googlesource.com/chromium/deps/libjpeg_turbo.git/+/e14cbfaa85529d47f9f55b0f104a579c1061f9ad/LICENSE.md?format=TEXT
3+
4+This software is based in part on the work of the Independent JPEG Group.
5+
6+libjpeg-turbo Licenses
7+======================
8+
9+libjpeg-turbo is covered by two compatible BSD-style open source licenses:
10+
11+- The IJG (Independent JPEG Group) License, which is listed in
12+ [README.ijg](README.ijg)
13+
14+ This license applies to the libjpeg API library and associated programs,
15+ including any code inherited from libjpeg and any modifications to that
16+ code. Note that the libjpeg-turbo SIMD source code bears the
17+ [zlib License](https://opensource.org/licenses/Zlib), but in the context of
18+ the overall libjpeg API library, the terms of the zlib License are subsumed
19+ by the terms of the IJG License.
20+
21+- The Modified (3-clause) BSD License, which is listed below
22+
23+ This license applies to the TurboJPEG API library and associated programs, as
24+ well as the build system. Note that the TurboJPEG API library wraps the
25+ libjpeg API library, so in the context of the overall TurboJPEG API library,
26+ both the terms of the IJG License and the terms of the Modified (3-clause)
27+ BSD License apply.
28+
29+
30+Complying with the libjpeg-turbo Licenses
31+=========================================
32+
33+This section provides a roll-up of the libjpeg-turbo licensing terms, to the
34+best of our understanding. This is not a license in and of itself. It is
35+intended solely for clarification.
36+
37+1. If you are distributing a modified version of the libjpeg-turbo source,
38+ then:
39+
40+ 1. You cannot alter or remove any existing copyright or license notices
41+ from the source.
42+
43+ **Origin**
44+ - Clause 1 of the IJG License
45+ - Clause 1 of the Modified BSD License
46+ - Clauses 1 and 3 of the zlib License
47+
48+ 2. You must add your own copyright notice to the header of each source
49+ file you modified, so others can tell that you modified that file. (If
50+ there is not an existing copyright header in that file, then you can
51+ simply add a notice stating that you modified the file.)
52+
53+ **Origin**
54+ - Clause 1 of the IJG License
55+ - Clause 2 of the zlib License
56+
57+ 3. You must include the IJG README file, and you must not alter any of the
58+ copyright or license text in that file.
59+
60+ **Origin**
61+ - Clause 1 of the IJG License
62+
63+2. If you are distributing only libjpeg-turbo binaries without the source, or
64+ if you are distributing an application that statically links with
65+ libjpeg-turbo, then:
66+
67+ 1. Your product documentation must include a message stating:
68+
69+ This software is based in part on the work of the Independent JPEG
70+ Group.
71+
72+ **Origin**
73+ - Clause 2 of the IJG license
74+
75+ 2. If your binary distribution includes or uses the TurboJPEG API, then
76+ your product documentation must include the text of the Modified BSD
77+ License (see below.)
78+
79+ **Origin**
80+ - Clause 2 of the Modified BSD License
81+
82+3. You cannot use the name of the IJG or The libjpeg-turbo Project or the
83+ contributors thereof in advertising, publicity, etc.
84+
85+ **Origin**
86+ - IJG License
87+ - Clause 3 of the Modified BSD License
88+
89+4. The IJG and The libjpeg-turbo Project do not warrant libjpeg-turbo to be
90+ free of defects, nor do we accept any liability for undesirable
91+ consequences resulting from your use of the software.
92+
93+ **Origin**
94+ - IJG License
95+ - Modified BSD License
96+ - zlib License
97+
98+
99+The Modified (3-clause) BSD License
100+===================================
101+
102+Copyright (C)2009-2024 D. R. Commander. All Rights Reserved.<br>
103+Copyright (C)2015 Viktor Szathmáry. All Rights Reserved.
104+
105+Redistribution and use in source and binary forms, with or without
106+modification, are permitted provided that the following conditions are met:
107+
108+- Redistributions of source code must retain the above copyright notice,
109+ this list of conditions and the following disclaimer.
110+- Redistributions in binary form must reproduce the above copyright notice,
111+ this list of conditions and the following disclaimer in the documentation
112+ and/or other materials provided with the distribution.
113+- Neither the name of the libjpeg-turbo Project nor the names of its
114+ contributors may be used to endorse or promote products derived from this
115+ software without specific prior written permission.
116+
117+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
118+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
119+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
120+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
121+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
122+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
123+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
124+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
125+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
126+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
127+POSSIBILITY OF SUCH DAMAGE.
128+
129+
130+Why Two Licenses?
131+=================
132+
133+The zlib License could have been used instead of the Modified (3-clause) BSD
134+License, and since the IJG License effectively subsumes the distribution
135+conditions of the zlib License, this would have effectively placed
136+libjpeg-turbo binary distributions under the IJG License. However, the IJG
137+License specifically refers to the Independent JPEG Group and does not extend
138+attribution and endorsement protections to other entities. Thus, it was
139+desirable to choose a license that granted us the same protections for new code
140+that were granted to the IJG for code derived from their software.
native/runtime/licenses/libpng.txtnew file+137
@@ -0,0 +1,137 @@
1+libpng: pinned Skia m153-0.101.2 native dependency.
2+Source: https://skia.googlesource.com/third_party/libpng/+/d5515b5b8be3901aac04e5bd8bd5c89f287bcd33/LICENSE?format=TEXT
3+
4+COPYRIGHT NOTICE, DISCLAIMER, and LICENSE
5+=========================================
6+
7+PNG Reference Library License version 2
8+---------------------------------------
9+
10+ * Copyright (c) 1995-2026 The PNG Reference Library Authors.
11+ * Copyright (c) 2018-2026 Cosmin Truta.
12+ * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.
13+ * Copyright (c) 1996-1997 Andreas Dilger.
14+ * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
15+
16+The software is supplied "as is", without warranty of any kind,
17+express or implied, including, without limitation, the warranties
18+of merchantability, fitness for a particular purpose, title, and
19+non-infringement. In no event shall the Copyright owners, or
20+anyone distributing the software, be liable for any damages or
21+other liability, whether in contract, tort or otherwise, arising
22+from, out of, or in connection with the software, or the use or
23+other dealings in the software, even if advised of the possibility
24+of such damage.
25+
26+Permission is hereby granted to use, copy, modify, and distribute
27+this software, or portions hereof, for any purpose, without fee,
28+subject to the following restrictions:
29+
30+ 1. The origin of this software must not be misrepresented; you
31+ must not claim that you wrote the original software. If you
32+ use this software in a product, an acknowledgment in the product
33+ documentation would be appreciated, but is not required.
34+
35+ 2. Altered source versions must be plainly marked as such, and must
36+ not be misrepresented as being the original software.
37+
38+ 3. This Copyright notice may not be removed or altered from any
39+ source or altered source distribution.
40+
41+
42+PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35)
43+-----------------------------------------------------------------------
44+
45+libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are
46+Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are
47+derived from libpng-1.0.6, and are distributed according to the same
48+disclaimer and license as libpng-1.0.6 with the following individuals
49+added to the list of Contributing Authors:
50+
51+ Simon-Pierre Cadieux
52+ Eric S. Raymond
53+ Mans Rullgard
54+ Cosmin Truta
55+ Gilles Vollant
56+ James Yu
57+ Mandar Sahastrabuddhe
58+ Google Inc.
59+ Vadim Barkov
60+
61+and with the following additions to the disclaimer:
62+
63+ There is no warranty against interference with your enjoyment of
64+ the library or against infringement. There is no warranty that our
65+ efforts or the library will fulfill any of your particular purposes
66+ or needs. This library is provided with all faults, and the entire
67+ risk of satisfactory quality, performance, accuracy, and effort is
68+ with the user.
69+
70+Some files in the "contrib" directory and some configure-generated
71+files that are distributed with libpng have other copyright owners, and
72+are released under other open source licenses.
73+
74+libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
75+Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from
76+libpng-0.96, and are distributed according to the same disclaimer and
77+license as libpng-0.96, with the following individuals added to the
78+list of Contributing Authors:
79+
80+ Tom Lane
81+ Glenn Randers-Pehrson
82+ Willem van Schaik
83+
84+libpng versions 0.89, June 1996, through 0.96, May 1997, are
85+Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88,
86+and are distributed according to the same disclaimer and license as
87+libpng-0.88, with the following individuals added to the list of
88+Contributing Authors:
89+
90+ John Bowler
91+ Kevin Bracey
92+ Sam Bushell
93+ Magnus Holmgren
94+ Greg Roelofs
95+ Tom Tanner
96+
97+Some files in the "scripts" directory have other copyright owners,
98+but are released under this license.
99+
100+libpng versions 0.5, May 1995, through 0.88, January 1996, are
101+Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
102+
103+For the purposes of this copyright and license, "Contributing Authors"
104+is defined as the following set of individuals:
105+
106+ Andreas Dilger
107+ Dave Martindale
108+ Guy Eric Schalnat
109+ Paul Schmidt
110+ Tim Wegner
111+
112+The PNG Reference Library is supplied "AS IS". The Contributing
113+Authors and Group 42, Inc. disclaim all warranties, expressed or
114+implied, including, without limitation, the warranties of
115+merchantability and of fitness for any purpose. The Contributing
116+Authors and Group 42, Inc. assume no liability for direct, indirect,
117+incidental, special, exemplary, or consequential damages, which may
118+result from the use of the PNG Reference Library, even if advised of
119+the possibility of such damage.
120+
121+Permission is hereby granted to use, copy, modify, and distribute this
122+source code, or portions hereof, for any purpose, without fee, subject
123+to the following restrictions:
124+
125+ 1. The origin of this source code must not be misrepresented.
126+
127+ 2. Altered versions must be plainly marked as such and must not
128+ be misrepresented as being the original source.
129+
130+ 3. This Copyright notice may not be removed or altered from any
131+ source or altered source distribution.
132+
133+The Contributing Authors and Group 42, Inc. specifically permit,
134+without fee, and encourage the use of this source code as a component
135+to supporting the PNG file format in commercial products. If you use
136+this source code in a product, acknowledgment is not required but would
137+be appreciated.
native/runtime/licenses/rust-skia.txtnew file+25
@@ -0,0 +1,25 @@
1+rust-skia / skia-safe / skia-bindings 0.153.3
2+Source: https://raw.githubusercontent.com/rust-skia/rust-skia/0.153.3/LICENSE
3+
4+MIT License
5+
6+Copyright (c) 2019 LongYinan & Armin Sander
7+Copyright (c) 2019 rust-skia Contributors
8+
9+Permission is hereby granted, free of charge, to any person obtaining a copy
10+of this software and associated documentation files (the "Software"), to deal
11+in the Software without restriction, including without limitation the rights
12+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+copies of the Software, and to permit persons to whom the Software is
14+furnished to do so, subject to the following conditions:
15+
16+The above copyright notice and this permission notice shall be included in all
17+copies or substantial portions of the Software.
18+
19+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+SOFTWARE.
native/runtime/licenses/skia.txtnew file+32
@@ -0,0 +1,32 @@
1+Skia m153-0.101.2, pinned by skia-bindings 0.153.3. This is the LICENSE_SKIA notice included in upstream binary-cache distributions.
2+Source: https://raw.githubusercontent.com/rust-skia/skia/m153-0.101.2/LICENSE
3+
4+Copyright (c) 2011 Google Inc. All rights reserved.
5+
6+Redistribution and use in source and binary forms, with or without
7+modification, are permitted provided that the following conditions are
8+met:
9+
10+ * Redistributions of source code must retain the above copyright
11+ notice, this list of conditions and the following disclaimer.
12+
13+ * Redistributions in binary form must reproduce the above copyright
14+ notice, this list of conditions and the following disclaimer in
15+ the documentation and/or other materials provided with the
16+ distribution.
17+
18+ * Neither the name of the copyright holder nor the names of its
19+ contributors may be used to endorse or promote products derived
20+ from this software without specific prior written permission.
21+
22+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
26+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
30+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
native/runtime/licenses/wuffs.txtnew file+205
@@ -0,0 +1,205 @@
1+wuffs: pinned Skia m153-0.101.2 native dependency.
2+Source: https://skia.googlesource.com/external/github.com/google/wuffs-mirror-release-c.git/+/e3f919ccfe3ef542cfc983a82146070258fb57f8/LICENSE?format=TEXT
3+
4+
5+ Apache License
6+ Version 2.0, January 2004
7+ http://www.apache.org/licenses/
8+
9+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
10+
11+ 1. Definitions.
12+
13+ "License" shall mean the terms and conditions for use, reproduction,
14+ and distribution as defined by Sections 1 through 9 of this document.
15+
16+ "Licensor" shall mean the copyright owner or entity authorized by
17+ the copyright owner that is granting the License.
18+
19+ "Legal Entity" shall mean the union of the acting entity and all
20+ other entities that control, are controlled by, or are under common
21+ control with that entity. For the purposes of this definition,
22+ "control" means (i) the power, direct or indirect, to cause the
23+ direction or management of such entity, whether by contract or
24+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
25+ outstanding shares, or (iii) beneficial ownership of such entity.
26+
27+ "You" (or "Your") shall mean an individual or Legal Entity
28+ exercising permissions granted by this License.
29+
30+ "Source" form shall mean the preferred form for making modifications,
31+ including but not limited to software source code, documentation
32+ source, and configuration files.
33+
34+ "Object" form shall mean any form resulting from mechanical
35+ transformation or translation of a Source form, including but
36+ not limited to compiled object code, generated documentation,
37+ and conversions to other media types.
38+
39+ "Work" shall mean the work of authorship, whether in Source or
40+ Object form, made available under the License, as indicated by a
41+ copyright notice that is included in or attached to the work
42+ (an example is provided in the Appendix below).
43+
44+ "Derivative Works" shall mean any work, whether in Source or Object
45+ form, that is based on (or derived from) the Work and for which the
46+ editorial revisions, annotations, elaborations, or other modifications
47+ represent, as a whole, an original work of authorship. For the purposes
48+ of this License, Derivative Works shall not include works that remain
49+ separable from, or merely link (or bind by name) to the interfaces of,
50+ the Work and Derivative Works thereof.
51+
52+ "Contribution" shall mean any work of authorship, including
53+ the original version of the Work and any modifications or additions
54+ to that Work or Derivative Works thereof, that is intentionally
55+ submitted to Licensor for inclusion in the Work by the copyright owner
56+ or by an individual or Legal Entity authorized to submit on behalf of
57+ the copyright owner. For the purposes of this definition, "submitted"
58+ means any form of electronic, verbal, or written communication sent
59+ to the Licensor or its representatives, including but not limited to
60+ communication on electronic mailing lists, source code control systems,
61+ and issue tracking systems that are managed by, or on behalf of, the
62+ Licensor for the purpose of discussing and improving the Work, but
63+ excluding communication that is conspicuously marked or otherwise
64+ designated in writing by the copyright owner as "Not a Contribution."
65+
66+ "Contributor" shall mean Licensor and any individual or Legal Entity
67+ on behalf of whom a Contribution has been received by Licensor and
68+ subsequently incorporated within the Work.
69+
70+ 2. Grant of Copyright License. Subject to the terms and conditions of
71+ this License, each Contributor hereby grants to You a perpetual,
72+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
73+ copyright license to reproduce, prepare Derivative Works of,
74+ publicly display, publicly perform, sublicense, and distribute the
75+ Work and such Derivative Works in Source or Object form.
76+
77+ 3. Grant of Patent License. Subject to the terms and conditions of
78+ this License, each Contributor hereby grants to You a perpetual,
79+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
80+ (except as stated in this section) patent license to make, have made,
81+ use, offer to sell, sell, import, and otherwise transfer the Work,
82+ where such license applies only to those patent claims licensable
83+ by such Contributor that are necessarily infringed by their
84+ Contribution(s) alone or by combination of their Contribution(s)
85+ with the Work to which such Contribution(s) was submitted. If You
86+ institute patent litigation against any entity (including a
87+ cross-claim or counterclaim in a lawsuit) alleging that the Work
88+ or a Contribution incorporated within the Work constitutes direct
89+ or contributory patent infringement, then any patent licenses
90+ granted to You under this License for that Work shall terminate
91+ as of the date such litigation is filed.
92+
93+ 4. Redistribution. You may reproduce and distribute copies of the
94+ Work or Derivative Works thereof in any medium, with or without
95+ modifications, and in Source or Object form, provided that You
96+ meet the following conditions:
97+
98+ (a) You must give any other recipients of the Work or
99+ Derivative Works a copy of this License; and
100+
101+ (b) You must cause any modified files to carry prominent notices
102+ stating that You changed the files; and
103+
104+ (c) You must retain, in the Source form of any Derivative Works
105+ that You distribute, all copyright, patent, trademark, and
106+ attribution notices from the Source form of the Work,
107+ excluding those notices that do not pertain to any part of
108+ the Derivative Works; and
109+
110+ (d) If the Work includes a "NOTICE" text file as part of its
111+ distribution, then any Derivative Works that You distribute must
112+ include a readable copy of the attribution notices contained
113+ within such NOTICE file, excluding those notices that do not
114+ pertain to any part of the Derivative Works, in at least one
115+ of the following places: within a NOTICE text file distributed
116+ as part of the Derivative Works; within the Source form or
117+ documentation, if provided along with the Derivative Works; or,
118+ within a display generated by the Derivative Works, if and
119+ wherever such third-party notices normally appear. The contents
120+ of the NOTICE file are for informational purposes only and
121+ do not modify the License. You may add Your own attribution
122+ notices within Derivative Works that You distribute, alongside
123+ or as an addendum to the NOTICE text from the Work, provided
124+ that such additional attribution notices cannot be construed
125+ as modifying the License.
126+
127+ You may add Your own copyright statement to Your modifications and
128+ may provide additional or different license terms and conditions
129+ for use, reproduction, or distribution of Your modifications, or
130+ for any such Derivative Works as a whole, provided Your use,
131+ reproduction, and distribution of the Work otherwise complies with
132+ the conditions stated in this License.
133+
134+ 5. Submission of Contributions. Unless You explicitly state otherwise,
135+ any Contribution intentionally submitted for inclusion in the Work
136+ by You to the Licensor shall be under the terms and conditions of
137+ this License, without any additional terms or conditions.
138+ Notwithstanding the above, nothing herein shall supersede or modify
139+ the terms of any separate license agreement you may have executed
140+ with Licensor regarding such Contributions.
141+
142+ 6. Trademarks. This License does not grant permission to use the trade
143+ names, trademarks, service marks, or product names of the Licensor,
144+ except as required for reasonable and customary use in describing the
145+ origin of the Work and reproducing the content of the NOTICE file.
146+
147+ 7. Disclaimer of Warranty. Unless required by applicable law or
148+ agreed to in writing, Licensor provides the Work (and each
149+ Contributor provides its Contributions) on an "AS IS" BASIS,
150+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
151+ implied, including, without limitation, any warranties or conditions
152+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
153+ PARTICULAR PURPOSE. You are solely responsible for determining the
154+ appropriateness of using or redistributing the Work and assume any
155+ risks associated with Your exercise of permissions under this License.
156+
157+ 8. Limitation of Liability. In no event and under no legal theory,
158+ whether in tort (including negligence), contract, or otherwise,
159+ unless required by applicable law (such as deliberate and grossly
160+ negligent acts) or agreed to in writing, shall any Contributor be
161+ liable to You for damages, including any direct, indirect, special,
162+ incidental, or consequential damages of any character arising as a
163+ result of this License or out of the use or inability to use the
164+ Work (including but not limited to damages for loss of goodwill,
165+ work stoppage, computer failure or malfunction, or any and all
166+ other commercial damages or losses), even if such Contributor
167+ has been advised of the possibility of such damages.
168+
169+ 9. Accepting Warranty or Additional Liability. While redistributing
170+ the Work or Derivative Works thereof, You may choose to offer,
171+ and charge a fee for, acceptance of support, warranty, indemnity,
172+ or other liability obligations and/or rights consistent with this
173+ License. However, in accepting such obligations, You may act only
174+ on Your own behalf and on Your sole responsibility, not on behalf
175+ of any other Contributor, and only if You agree to indemnify,
176+ defend, and hold each Contributor harmless for any liability
177+ incurred by, or claims asserted against, such Contributor by reason
178+ of your accepting any such warranty or additional liability.
179+
180+ END OF TERMS AND CONDITIONS
181+
182+ APPENDIX: How to apply the Apache License to your work.
183+
184+ To apply the Apache License to your work, attach the following
185+ boilerplate notice, with the fields enclosed by brackets "[]"
186+ replaced with your own identifying information. (Don't include
187+ the brackets!) The text should be enclosed in the appropriate
188+ comment syntax for the file format. We also recommend that a
189+ file or class name and description of purpose be included on the
190+ same "printed page" as the copyright notice for easier
191+ identification within third-party archives.
192+
193+ Copyright [yyyy] [name of copyright owner]
194+
195+ Licensed under the Apache License, Version 2.0 (the "License");
196+ you may not use this file except in compliance with the License.
197+ You may obtain a copy of the License at
198+
199+ http://www.apache.org/licenses/LICENSE-2.0
200+
201+ Unless required by applicable law or agreed to in writing, software
202+ distributed under the License is distributed on an "AS IS" BASIS,
203+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
204+ See the License for the specific language governing permissions and
205+ limitations under the License.
native/runtime/licenses/zlib.txtnew file+22
@@ -0,0 +1,22 @@
1+zlib: pinned Skia m153-0.101.2 native dependency.
2+Source: https://chromium.googlesource.com/chromium/src/third_party/zlib/+/646b7f569718921d7d4b5b8e22572ff6c76f2596/LICENSE?format=TEXT
3+
4+version 1.2.12, March 27th, 2022
5+
6+Copyright (C) 1995-2022 Jean-loup Gailly and Mark Adler
7+
8+This software is provided 'as-is', without any express or implied
9+warranty. In no event will the authors be held liable for any damages
10+arising from the use of this software.
11+
12+Permission is granted to anyone to use this software for any purpose,
13+including commercial applications, and to alter it and redistribute it
14+freely, subject to the following restrictions:
15+
16+1. The origin of this software must not be misrepresented; you must not
17+ claim that you wrote the original software. If you use this software
18+ in a product, an acknowledgment in the product documentation would be
19+ appreciated but is not required.
20+2. Altered source versions must be plainly marked as such, and must not be
21+ misrepresented as being the original software.
22+3. This notice may not be removed or altered from any source distribution.
native/runtime/src/graphics.rsnew file+892
@@ -0,0 +1,892 @@
1+// Surface/path/paint/readback patterns adapted from fframes at
2+// 4ffb052d3430e0672b4f3647450c181606ddf418. See ../licenses/fframes.txt.
3+use std::collections::HashMap;
4+use std::str::FromStr;
5+use std::sync::Arc;
6+
7+use fontdb::{Database, Family, Query, Source, Stretch, Style, Weight, ID};
8+use rustybuzz::{Face, Feature, UnicodeBuffer};
9+use skia_safe::canvas::SaveLayerRec;
10+use skia_safe::{
11+ color_filters, image_filters, paint, surfaces, AlphaType, BlendMode, Canvas, ClipOp, ColorType,
12+ Data, FilterMode, Font, FontHinting, FontMgr, ImageInfo, Matrix, Paint, PaintStyle, Path,
13+ PathBuilder, PathFillType, PictureRecorder, Point, Rect, Surface, TileMode, Typeface,
14+};
15+use unicode_script::{Script, UnicodeScript};
16+use unicode_segmentation::UnicodeSegmentation;
17+
18+use crate::{failure, FontAsset, HostResult};
19+
20+#[derive(Default)]
21+pub(crate) struct Metrics {
22+ pub width: f64,
23+ pub ascent: f64,
24+ pub descent: f64,
25+}
26+
27+pub(crate) type Color = [u8; 4];
28+
29+pub(crate) struct Stroke {
30+ pub color: Color,
31+ pub width: f32,
32+ pub join: i32,
33+ pub cap: i32,
34+}
35+
36+pub(crate) struct CirclePattern {
37+ pub rect: [f32; 4],
38+ pub background: Color,
39+ pub ink: Color,
40+ pub tile: f32,
41+ pub radius: f32,
42+ pub origin: [f32; 2],
43+}
44+#[derive(Clone)]
45+struct DrawStyle {
46+ blend: BlendMode,
47+ blur: f32,
48+ tint: Option<skia_safe::ColorFilter>,
49+}
50+
51+struct Raster {
52+ surface: Surface,
53+ style: DrawStyle,
54+ saved: Vec<DrawStyle>,
55+}
56+
57+struct RequestedFont {
58+ size: f64,
59+ weight: u16,
60+ style: Style,
61+ small_caps: bool,
62+}
63+
64+struct GlyphRun {
65+ id: ID,
66+ size: f32,
67+ embolden: bool,
68+ skew_x: f32,
69+ glyphs: Vec<u16>,
70+ positions: Vec<Point>,
71+}
72+
73+pub(crate) struct Graphics {
74+ fonts: Arc<Database>,
75+ features: HashMap<ID, Vec<Feature>>,
76+ font_manager: FontMgr,
77+ typefaces: HashMap<ID, Typeface>,
78+ surfaces: HashMap<u32, Raster>,
79+ next_surface: u32,
80+}
81+
82+impl Graphics {
83+ pub fn new(assets: &[FontAsset]) -> HostResult<Self> {
84+ let mut fonts = Database::new();
85+ let mut features = HashMap::new();
86+ for asset in assets {
87+ let ids = fonts.load_font_source(Source::Binary(Arc::new(asset.data)));
88+ if ids.is_empty() {
89+ return Err(failure(format!("invalid font asset: {}", asset.family)));
90+ }
91+ let parsed_features = asset
92+ .features
93+ .split(',')
94+ .filter(|part| !part.trim().is_empty())
95+ .map(|part| {
96+ Feature::from_str(part.trim())
97+ .map_err(|_| failure(format!("invalid OpenType feature: {part}")))
98+ })
99+ .collect::<HostResult<Vec<_>>>()?;
100+ for id in ids {
101+ let mut face = fonts
102+ .face(id)
103+ .ok_or_else(|| failure("loaded font face is unavailable"))?
104+ .clone();
105+ fonts.remove_face(id);
106+ // Explicit aliases and metadata are authoritative. The same bytes
107+ // may be registered under several CSS names without copying them.
108+ face.families.insert(
109+ 0,
110+ (asset.family.into(), fontdb::Language::English_UnitedStates),
111+ );
112+ face.weight = Weight(asset.weight);
113+ face.style = if asset.italic {
114+ Style::Italic
115+ } else {
116+ Style::Normal
117+ };
118+ let id = fonts.push_face_info(face);
119+ features.insert(id, parsed_features.clone());
120+ }
121+ }
122+ // Supplied faces precede system faces in otherwise identical CSS matches.
123+ fonts.load_system_fonts();
124+ if fonts.is_empty() {
125+ return Err(failure("no fonts are available"));
126+ }
127+ Ok(Self {
128+ fonts: Arc::new(fonts),
129+ features,
130+ font_manager: FontMgr::new(),
131+ typefaces: HashMap::new(),
132+ surfaces: HashMap::new(),
133+ next_surface: 1,
134+ })
135+ }
136+
137+ pub fn measure(&self, shorthand: &str, text: &str) -> HostResult<Metrics> {
138+ self.shape_text(shorthand, text, None)
139+ .map(|(metrics, _, _)| metrics)
140+ }
141+
142+ fn shape_text(
143+ &self,
144+ shorthand: &str,
145+ text: &str,
146+ mut runs: Option<&mut Vec<GlyphRun>>,
147+ ) -> HostResult<(Metrics, ID, f64)> {
148+ let font = svgtypes::FontShorthand::from_str(shorthand)
149+ .map_err(|error| failure(format!("invalid CSS font {shorthand:?}: {error}")))?;
150+ let size = font_size(font.font_size)?;
151+ let families = svgtypes::parse_font_families(font.font_family)?;
152+ let names: Vec<_> = families
153+ .iter()
154+ .map(|family| match family {
155+ svgtypes::FontFamily::Named(name) => Family::Name(name),
156+ svgtypes::FontFamily::Serif => Family::Serif,
157+ svgtypes::FontFamily::SansSerif => Family::SansSerif,
158+ svgtypes::FontFamily::Monospace => Family::Monospace,
159+ svgtypes::FontFamily::Cursive => Family::Cursive,
160+ svgtypes::FontFamily::Fantasy => Family::Fantasy,
161+ })
162+ .collect();
163+ let weight = match font.font_weight.unwrap_or("normal") {
164+ "normal" => 400,
165+ "bold" | "bolder" => 700,
166+ "lighter" => 300,
167+ value => value.parse::<u16>()?,
168+ };
169+ let style = match font.font_style {
170+ Some("italic") => Style::Italic,
171+ Some("oblique") => Style::Oblique,
172+ _ => Style::Normal,
173+ };
174+ let stretch = match font.font_stretch {
175+ Some("ultra-condensed") => Stretch::UltraCondensed,
176+ Some("extra-condensed") => Stretch::ExtraCondensed,
177+ Some("condensed") => Stretch::Condensed,
178+ Some("semi-condensed") => Stretch::SemiCondensed,
179+ Some("semi-expanded") => Stretch::SemiExpanded,
180+ Some("expanded") => Stretch::Expanded,
181+ Some("extra-expanded") => Stretch::ExtraExpanded,
182+ Some("ultra-expanded") => Stretch::UltraExpanded,
183+ _ => Stretch::Normal,
184+ };
185+ let base = self
186+ .fonts
187+ .query(&Query {
188+ families: &names,
189+ weight: Weight(weight),
190+ stretch,
191+ style,
192+ })
193+ .or_else(|| {
194+ self.fonts.query(&Query {
195+ families: &[Family::SansSerif],
196+ weight: Weight(weight),
197+ stretch,
198+ style,
199+ })
200+ })
201+ .or_else(|| self.fonts.faces().next().map(|face| face.id))
202+ .ok_or_else(|| failure(format!("no font matches {shorthand:?}")))?;
203+ let requested = RequestedFont {
204+ size,
205+ weight,
206+ style,
207+ small_caps: font.font_variant == Some("small-caps"),
208+ };
209+ let mut metrics = Metrics::default();
210+ let mut run_start = 0;
211+ let mut run_font = base;
212+ let mut run_script = Script::Common;
213+ // Keep combining marks and emoji sequences together during fallback.
214+ // Script changes split shaping runs, but common punctuation and inherited
215+ // marks retain the surrounding script instead of breaking ligatures.
216+ for (offset, grapheme) in text.grapheme_indices(true) {
217+ let selected = self.fallback(base, grapheme);
218+ let script = grapheme
219+ .chars()
220+ .map(|c| c.script())
221+ .find(|script| !matches!(script, Script::Common | Script::Inherited))
222+ .unwrap_or(run_script);
223+ if offset > run_start
224+ && (selected != run_font || (script != run_script && run_script != Script::Common))
225+ {
226+ self.shape(
227+ run_font,
228+ &text[run_start..offset],
229+ &requested,
230+ &mut metrics,
231+ runs.as_deref_mut(),
232+ )?;
233+ run_start = offset;
234+ }
235+ run_font = selected;
236+ run_script = script;
237+ }
238+ if run_start < text.len() {
239+ self.shape(
240+ run_font,
241+ &text[run_start..],
242+ &requested,
243+ &mut metrics,
244+ runs.as_deref_mut(),
245+ )?;
246+ }
247+ Ok((metrics, base, size))
248+ }
249+
250+ fn supports(&self, id: ID, text: &str) -> bool {
251+ self.fonts.with_face_data(id, |data, index| {
252+ Face::from_slice(data, index).map(|face| text.chars().all(|c| {
253+ // Joiners and variation selectors modify their surrounding glyphs.
254+ matches!(c, '\u{200c}' | '\u{200d}' | '\u{fe00}'..='\u{fe0f}' | '\u{e0100}'..='\u{e01ef}')
255+ || face.glyph_index(c).is_some()
256+ })).unwrap_or(false)
257+ }).unwrap_or(false)
258+ }
259+
260+ fn fallback(&self, base: ID, text: &str) -> ID {
261+ if self.supports(base, text) {
262+ return base;
263+ }
264+ let original = self.fonts.face(base);
265+ self.fonts
266+ .faces()
267+ .filter(|face| {
268+ original
269+ .map(|original| {
270+ face.style == original.style
271+ || face.weight == original.weight
272+ || face.stretch == original.stretch
273+ })
274+ .unwrap_or(true)
275+ })
276+ .find(|face| self.supports(face.id, text))
277+ .map(|face| face.id)
278+ .or_else(|| {
279+ self.fonts
280+ .faces()
281+ .find(|face| self.supports(face.id, text))
282+ .map(|face| face.id)
283+ })
284+ .unwrap_or(base)
285+ }
286+
287+ fn shape(
288+ &self,
289+ id: ID,
290+ text: &str,
291+ requested: &RequestedFont,
292+ metrics: &mut Metrics,
293+ runs: Option<&mut Vec<GlyphRun>>,
294+ ) -> HostResult<()> {
295+ let selected = self
296+ .fonts
297+ .face(id)
298+ .ok_or_else(|| failure("font face is unavailable"))?;
299+ let embolden = requested.weight >= 600 && selected.weight.0 < 600;
300+ let skew_x = if requested.style != Style::Normal && selected.style == Style::Normal {
301+ -0.21255656
302+ } else {
303+ 0.0
304+ };
305+ self.fonts
306+ .with_face_data(id, |data, index| -> HostResult<()> {
307+ let face = Face::from_slice(data, index)
308+ .ok_or_else(|| failure("font could not be shaped"))?;
309+ let scale = requested.size / f64::from(face.units_per_em());
310+ let mut buffer = UnicodeBuffer::new();
311+ buffer.push_str(text);
312+ buffer.guess_segment_properties();
313+ let normal = self.features.get(&id).map(Vec::as_slice).unwrap_or(&[]);
314+ let mut caps = Vec::new();
315+ let features = if requested.small_caps {
316+ caps.extend_from_slice(normal);
317+ caps.push(
318+ Feature::from_str("smcp")
319+ .map_err(|_| failure("invalid small-caps feature"))?,
320+ );
321+ caps.as_slice()
322+ } else {
323+ normal
324+ };
325+ let glyphs = rustybuzz::shape(&face, features, buffer);
326+ let mut advance = 0i64;
327+ let mut y_advance = 0i64;
328+ let mut run = runs.as_ref().map(|_| GlyphRun {
329+ id,
330+ size: requested.size as f32,
331+ embolden,
332+ skew_x,
333+ glyphs: Vec::with_capacity(glyphs.len()),
334+ positions: Vec::with_capacity(glyphs.len()),
335+ });
336+ let total_advance: i64 = glyphs
337+ .glyph_positions()
338+ .iter()
339+ .map(|position| i64::from(position.x_advance))
340+ .sum();
341+ let origin = metrics.width - (total_advance.min(0) as f64) * scale;
342+ for (info, position) in glyphs.glyph_infos().iter().zip(glyphs.glyph_positions()) {
343+ if let Some(run) = &mut run {
344+ run.glyphs.push(info.glyph_id as u16);
345+ run.positions.push(Point::new(
346+ (origin + (advance + i64::from(position.x_offset)) as f64 * scale)
347+ as f32,
348+ (-(y_advance + i64::from(position.y_offset)) as f64 * scale) as f32,
349+ ));
350+ }
351+ y_advance += i64::from(position.y_advance);
352+ advance += i64::from(position.x_advance);
353+ if let Some(bounds) = face
354+ .glyph_bounding_box(rustybuzz::ttf_parser::GlyphId(info.glyph_id as u16))
355+ {
356+ metrics.ascent = metrics
357+ .ascent
358+ .max(f64::from(i32::from(bounds.y_max) + position.y_offset) * scale);
359+ metrics.descent = metrics
360+ .descent
361+ .max(-f64::from(i32::from(bounds.y_min) + position.y_offset) * scale);
362+ }
363+ }
364+ metrics.width += (advance as f64).abs() * scale;
365+ if let (Some(runs), Some(run)) = (runs, run) {
366+ runs.push(run);
367+ }
368+ Ok(())
369+ })
370+ .ok_or_else(|| failure("font bytes are unavailable"))?
371+ }
372+
373+ fn raster(&mut self, id: u32) -> HostResult<&mut Raster> {
374+ self.surfaces
375+ .get_mut(&id)
376+ .ok_or_else(|| failure(format!("unknown canvas surface: {id}")))
377+ }
378+
379+ pub fn create(&mut self, width: i32, height: i32) -> HostResult<u32> {
380+ rgba_size(width, height)?;
381+ let id = self.next_surface;
382+ let next = id
383+ .checked_add(1)
384+ .ok_or_else(|| failure("canvas handles exhausted"))?;
385+ let mut surface = surfaces::raster_n32_premul((width, height))
386+ .ok_or_else(|| failure(format!("could not allocate {width}x{height} canvas")))?;
387+ // Preserve an untouched root clip so reset can discard every user clip.
388+ surface.canvas().save();
389+ self.surfaces.insert(
390+ id,
391+ Raster {
392+ surface,
393+ style: DrawStyle::default(),
394+ saved: Vec::new(),
395+ },
396+ );
397+ self.next_surface = next;
398+ Ok(id)
399+ }
400+
401+ pub fn release(&mut self, id: u32) -> HostResult<()> {
402+ self.surfaces
403+ .remove(&id)
404+ .ok_or_else(|| failure(format!("unknown canvas surface: {id}")))?;
405+ Ok(())
406+ }
407+
408+ pub fn reset(&mut self, id: u32) -> HostResult<()> {
409+ let raster = self.raster(id)?;
410+ let canvas = raster.surface.canvas();
411+ canvas.restore_to_count(1);
412+ canvas.reset_matrix();
413+ canvas.clear(skia_safe::Color::TRANSPARENT);
414+ canvas.save();
415+ raster.style = DrawStyle::default();
416+ raster.saved.clear();
417+ Ok(())
418+ }
419+
420+ pub fn clear(&mut self, id: u32, color: Color) -> HostResult<()> {
421+ self.raster(id)?.surface.canvas().clear(skia_color(color));
422+ Ok(())
423+ }
424+
425+ pub fn save(&mut self, id: u32) -> HostResult<()> {
426+ let raster = self.raster(id)?;
427+ raster.saved.push(raster.style.clone());
428+ raster.surface.canvas().save();
429+ Ok(())
430+ }
431+
432+ pub fn restore(&mut self, id: u32) -> HostResult<()> {
433+ let raster = self.raster(id)?;
434+ raster.style = raster
435+ .saved
436+ .pop()
437+ .ok_or_else(|| failure("canvas restore without matching save"))?;
438+ raster.surface.canvas().restore();
439+ Ok(())
440+ }
441+
442+ pub fn set_transform(&mut self, id: u32, matrix: [f32; 6]) -> HostResult<()> {
443+ let matrix = affine(matrix)?;
444+ self.raster(id)?.surface.canvas().set_matrix(&matrix.into());
445+ Ok(())
446+ }
447+
448+ pub fn transform(&mut self, id: u32, matrix: [f32; 6]) -> HostResult<()> {
449+ let matrix = affine(matrix)?;
450+ self.raster(id)?.surface.canvas().concat(&matrix);
451+ Ok(())
452+ }
453+
454+ pub fn clip(&mut self, id: u32, path: &Path, even_odd: bool) -> HostResult<()> {
455+ // SkPath clones share their immutable point/verb storage.
456+ let mut clip = path.clone();
457+ clip.set_fill_type(if even_odd {
458+ PathFillType::EvenOdd
459+ } else {
460+ PathFillType::Winding
461+ });
462+ self.raster(id)?
463+ .surface
464+ .canvas()
465+ .clip_path(&clip, ClipOp::Intersect, true);
466+ Ok(())
467+ }
468+
469+ pub fn set_blend(&mut self, id: u32, mode: &str) -> HostResult<()> {
470+ let blend = match mode {
471+ "source-over" => BlendMode::SrcOver,
472+ "difference" => BlendMode::Difference,
473+ "destination-out" => BlendMode::DstOut,
474+ "destination-in" => BlendMode::DstIn,
475+ "source-in" => BlendMode::SrcIn,
476+ "copy" => BlendMode::Src,
477+ _ => return Err(failure(format!("unknown canvas blend mode: {mode}"))),
478+ };
479+ self.raster(id)?.style.blend = blend;
480+ Ok(())
481+ }
482+
483+ pub fn set_blur(&mut self, id: u32, radius: f32) -> HostResult<()> {
484+ nonnegative(radius, "blur radius")?;
485+ self.raster(id)?.style.blur = radius;
486+ Ok(())
487+ }
488+
489+ pub fn set_tint(&mut self, id: u32, tint: Option<Color>) -> HostResult<()> {
490+ let filter = tint
491+ .map(|color| {
492+ color_filters::blend(skia_color(color), BlendMode::SrcIn)
493+ .ok_or_else(|| failure("could not create canvas tint"))
494+ })
495+ .transpose()?;
496+ self.raster(id)?.style.tint = filter;
497+ Ok(())
498+ }
499+
500+ pub fn fill_path(&mut self, id: u32, path: &Path, color: Color) -> HostResult<()> {
501+ self.raster(id)?.draw(|canvas, mut paint| {
502+ paint.set_color(skia_color(color));
503+ canvas.draw_path(path, &paint);
504+ })
505+ }
506+
507+ pub fn stroke_path(&mut self, id: u32, path: &Path, stroke: Stroke) -> HostResult<()> {
508+ let (join, cap) = stroke_style(&stroke)?;
509+ self.raster(id)?.draw(|canvas, mut paint| {
510+ set_stroke(&mut paint, &stroke, join, cap);
511+ canvas.draw_path(path, &paint);
512+ })
513+ }
514+
515+ pub fn fill_stroke_path(
516+ &mut self,
517+ id: u32,
518+ path: &Path,
519+ color: Color,
520+ stroke: Stroke,
521+ ) -> HostResult<()> {
522+ // Validate the entire operation before painting its first component.
523+ stroke_style(&stroke)?;
524+ self.fill_path(id, path, color)?;
525+ self.stroke_path(id, path, stroke)
526+ }
527+
528+ pub fn text(
529+ &mut self,
530+ id: u32,
531+ font: &str,
532+ text: &str,
533+ x: f32,
534+ y: f32,
535+ color: Color,
536+ align: i32,
537+ baseline: i32,
538+ ) -> HostResult<()> {
539+ finite(&[x, y], "text position")?;
540+ self.raster(id)?;
541+ if !(0..=2).contains(&align) {
542+ return Err(failure(format!("unknown canvas text alignment: {align}")));
543+ }
544+ if !(0..=3).contains(&baseline) {
545+ return Err(failure(format!("unknown canvas text baseline: {baseline}")));
546+ }
547+ let mut runs = Vec::new();
548+ let (metrics, base, size) = self.shape_text(font, text, Some(&mut runs))?;
549+ let x = x - match align {
550+ 1 => metrics.width as f32 / 2.0,
551+ 2 => metrics.width as f32,
552+ _ => 0.0,
553+ };
554+ for id in std::iter::once(base).chain(runs.iter().map(|run| run.id)) {
555+ if !self.typefaces.contains_key(&id) {
556+ let face = self
557+ .fonts
558+ .with_face_data(id, |data, index| {
559+ self.font_manager.new_from_data(Data::new_copy(data), index)
560+ })
561+ .flatten()
562+ .ok_or_else(|| failure("Skia could not load the shaped font face"))?;
563+ self.typefaces.insert(id, face);
564+ }
565+ }
566+ let typefaces = &self.typefaces;
567+ // Canvas baselines belong to the font, not to the current word's ink.
568+ // Otherwise a reveal from "H" to "Hg" moves the already-visible H.
569+ let y = if baseline == 2 {
570+ y
571+ } else {
572+ let (_, line) = Font::from_typeface(typefaces[&base].clone(), size as f32).metrics();
573+ y + match baseline {
574+ 0 => -line.ascent,
575+ 1 => -(line.ascent + line.descent) / 2.0,
576+ _ => -line.descent,
577+ }
578+ };
579+ let raster = self
580+ .surfaces
581+ .get_mut(&id)
582+ .ok_or_else(|| failure(format!("unknown canvas surface: {id}")))?;
583+ raster.draw(|canvas, mut paint| {
584+ paint.set_color(skia_color(color));
585+ for run in &runs {
586+ let mut font = Font::from_typeface(typefaces[&run.id].clone(), run.size);
587+ // Keep fractional Rustybuzz placement; no independent hinted advances.
588+ font.set_subpixel(true)
589+ .set_hinting(FontHinting::None)
590+ .set_embolden(run.embolden)
591+ .set_skew_x(run.skew_x);
592+ canvas.draw_glyphs_at(&run.glyphs, run.positions.as_slice(), (x, y), &font, &paint);
593+ }
594+ })
595+ }
596+
597+ pub fn circle_pattern(&mut self, id: u32, pattern: CirclePattern) -> HostResult<()> {
598+ finite(&pattern.rect, "pattern rectangle")?;
599+ finite(&pattern.origin, "pattern origin")?;
600+ nonnegative(pattern.radius, "pattern radius")?;
601+ nonnegative(pattern.rect[2], "pattern width")?;
602+ nonnegative(pattern.rect[3], "pattern height")?;
603+ if !pattern.tile.is_finite() || pattern.tile <= 0.0 {
604+ return Err(failure("pattern tile must be positive and finite"));
605+ }
606+ // Record just one periodic tile, not one FFI call or picture per screen dot.
607+ let half = pattern.tile / 2.0;
608+ let tile = Rect::from_xywh(-half, -half, pattern.tile, pattern.tile);
609+ let mut recorder = PictureRecorder::new();
610+ let canvas = recorder.begin_recording(tile, false);
611+ canvas.clear(skia_color(pattern.background));
612+ let mut paint = Paint::default();
613+ paint
614+ .set_anti_alias(true)
615+ .set_color(skia_color(pattern.ink));
616+ // Neighbouring circles must contribute when a radius crosses a tile edge.
617+ let reach = ((pattern.radius as f64 / pattern.tile as f64) + 0.5).floor();
618+ if reach > i32::MAX as f64 {
619+ return Err(failure(
620+ "pattern radius/tile ratio exceeds native coordinate range",
621+ ));
622+ }
623+ let reach = reach as i32;
624+ for row in -reach..=reach {
625+ for column in -reach..=reach {
626+ canvas.draw_circle(
627+ (column as f32 * pattern.tile, row as f32 * pattern.tile),
628+ pattern.radius,
629+ &paint,
630+ );
631+ }
632+ }
633+ let picture = recorder
634+ .finish_recording_as_picture(None)
635+ .ok_or_else(|| failure("could not record circle tile"))?;
636+ let matrix = Matrix::translate((pattern.origin[0], pattern.origin[1]));
637+ let shader = picture.to_shader(
638+ (TileMode::Repeat, TileMode::Repeat),
639+ FilterMode::Linear,
640+ &matrix,
641+ &tile,
642+ );
643+ let [x, y, width, height] = pattern.rect;
644+ self.raster(id)?.draw(|canvas, mut paint| {
645+ paint.set_shader(shader);
646+ canvas.draw_rect(Rect::from_xywh(x, y, width, height), &paint);
647+ })
648+ }
649+
650+ pub fn surface(&mut self, destination: u32, source: u32) -> HostResult<()> {
651+ self.raster(destination)?;
652+ // Raster snapshots share pixels until a writer needs copy-on-write. In
653+ // the normal distinct-surface case this draws without a full-frame copy.
654+ let image = self.raster(source)?.surface.image_snapshot();
655+ self.raster(destination)?.draw(|canvas, paint| {
656+ canvas.draw_image(&image, (0.0, 0.0), Some(&paint));
657+ })
658+ }
659+
660+ pub fn png(&mut self, id: u32, path: &str) -> HostResult<()> {
661+ let pixels = self
662+ .raster(id)?
663+ .surface
664+ .peek_pixels()
665+ .ok_or_else(|| failure("canvas raster pixels are unavailable"))?;
666+ let mut file = std::fs::File::create(path)?;
667+ if !skia_safe::png_encoder::encode(&pixels, &mut file, &Default::default()) {
668+ return Err(failure(format!("could not encode canvas PNG: {path}")));
669+ }
670+ Ok(())
671+ }
672+
673+ pub fn read_rgba(
674+ &mut self,
675+ id: u32,
676+ width: i32,
677+ height: i32,
678+ output: &mut [u8],
679+ ) -> HostResult<()> {
680+ let size = rgba_size(width, height)?;
681+ let raster = self.raster(id)?;
682+ if raster.surface.width() != width || raster.surface.height() != height {
683+ return Err(failure(format!(
684+ "canvas is {}x{}, expected {width}x{height}",
685+ raster.surface.width(),
686+ raster.surface.height(),
687+ )));
688+ }
689+ if output.len() != size {
690+ return Err(failure(format!(
691+ "RGBA buffer has {} bytes, expected {size}",
692+ output.len()
693+ )));
694+ }
695+ let info = ImageInfo::new(
696+ (width, height),
697+ ColorType::RGBA8888,
698+ AlphaType::Unpremul,
699+ None,
700+ );
701+ if !raster
702+ .surface
703+ .read_pixels(&info, output, width as usize * 4, (0, 0))
704+ {
705+ return Err(failure("could not read straight RGBA canvas pixels"));
706+ }
707+ Ok(())
708+ }
709+}
710+
711+impl Default for DrawStyle {
712+ fn default() -> Self {
713+ Self {
714+ blend: BlendMode::SrcOver,
715+ blur: 0.0,
716+ tint: None,
717+ }
718+ }
719+}
720+
721+impl Raster {
722+ fn draw(&mut self, draw: impl FnOnce(&Canvas, Paint)) -> HostResult<()> {
723+ let mut paint = Paint::default();
724+ paint.set_anti_alias(true);
725+ paint.set_blend_mode(self.style.blend);
726+ paint.set_color_filter(self.style.tint.clone());
727+ let canvas = self.surface.canvas();
728+ if self.style.blur == 0.0 {
729+ draw(canvas, paint);
730+ } else {
731+ let filter = image_filters::blur(
732+ (self.style.blur, self.style.blur),
733+ TileMode::Decal,
734+ None,
735+ None,
736+ )
737+ .ok_or_else(|| failure("could not create canvas blur"))?;
738+ paint.set_image_filter(filter);
739+ // Establish the filter layer in device space, then restore the
740+ // geometry transform inside it. Sigma never inherits a zoom/shear.
741+ let matrix = canvas.local_to_device();
742+ let count = canvas.save();
743+ canvas.reset_matrix();
744+ canvas.save_layer(&SaveLayerRec::default().paint(&paint));
745+ canvas.set_matrix(&matrix);
746+ let mut content = Paint::default();
747+ content.set_anti_alias(true);
748+ draw(canvas, content);
749+ canvas.restore_to_count(count);
750+ }
751+ Ok(())
752+ }
753+}
754+
755+pub(crate) fn path_from_values(
756+ mut values: impl Iterator<Item = HostResult<f64>>,
757+) -> HostResult<Path> {
758+ fn coordinate(values: &mut impl Iterator<Item = HostResult<f64>>) -> HostResult<f32> {
759+ let value = values
760+ .next()
761+ .ok_or_else(|| failure("truncated canvas path command"))??;
762+ let coordinate = value as f32;
763+ if !coordinate.is_finite() {
764+ return Err(failure(
765+ "canvas path coordinates must be finite native numbers",
766+ ));
767+ }
768+ Ok(coordinate)
769+ }
770+ fn point(values: &mut impl Iterator<Item = HostResult<f64>>) -> HostResult<Point> {
771+ Ok(Point::new(coordinate(values)?, coordinate(values)?))
772+ }
773+ let mut builder = PathBuilder::new();
774+ let mut has_move = false;
775+ while let Some(tag) = values.next() {
776+ let tag = tag?;
777+ if tag != 1.0 && !has_move && (2.0..=5.0).contains(&tag) {
778+ return Err(failure("canvas path must begin with move-to"));
779+ }
780+ match tag {
781+ 1.0 => {
782+ builder.move_to(point(&mut values)?);
783+ has_move = true;
784+ }
785+ 2.0 => {
786+ builder.line_to(point(&mut values)?);
787+ }
788+ 3.0 => {
789+ let control = point(&mut values)?;
790+ builder.quad_to(control, point(&mut values)?);
791+ }
792+ 4.0 => {
793+ let first = point(&mut values)?;
794+ let second = point(&mut values)?;
795+ builder.cubic_to(first, second, point(&mut values)?);
796+ }
797+ 5.0 => {
798+ builder.close();
799+ }
800+ _ => return Err(failure(format!("unknown canvas path command: {tag}"))),
801+ }
802+ }
803+ Ok(builder.detach())
804+}
805+
806+fn skia_color([r, g, b, a]: Color) -> skia_safe::Color {
807+ skia_safe::Color::from_argb(a, r, g, b)
808+}
809+
810+fn finite(values: &[f32], name: &str) -> HostResult<()> {
811+ if values.iter().all(|value| value.is_finite()) {
812+ Ok(())
813+ } else {
814+ Err(failure(format!(
815+ "{name} must contain finite native numbers"
816+ )))
817+ }
818+}
819+
820+fn nonnegative(value: f32, name: &str) -> HostResult<()> {
821+ if value.is_finite() && value >= 0.0 {
822+ Ok(())
823+ } else {
824+ Err(failure(format!("{name} must be finite and nonnegative")))
825+ }
826+}
827+
828+fn affine([a, b, c, d, e, f]: [f32; 6]) -> HostResult<Matrix> {
829+ finite(&[a, b, c, d, e, f], "canvas transform")?;
830+ Ok(Matrix::new_all(a, c, e, b, d, f, 0.0, 0.0, 1.0))
831+}
832+
833+fn stroke_style(stroke: &Stroke) -> HostResult<(paint::Join, paint::Cap)> {
834+ nonnegative(stroke.width, "stroke width")?;
835+ let join = match stroke.join {
836+ 0 => paint::Join::Round,
837+ 1 => paint::Join::Bevel,
838+ 2 => paint::Join::Miter,
839+ value => return Err(failure(format!("unknown canvas stroke join: {value}"))),
840+ };
841+ let cap = match stroke.cap {
842+ 0 => paint::Cap::Butt,
843+ 1 => paint::Cap::Round,
844+ 2 => paint::Cap::Square,
845+ value => return Err(failure(format!("unknown canvas stroke cap: {value}"))),
846+ };
847+ Ok((join, cap))
848+}
849+
850+fn set_stroke(paint: &mut Paint, stroke: &Stroke, join: paint::Join, cap: paint::Cap) {
851+ paint.set_color(skia_color(stroke.color));
852+ paint.set_style(PaintStyle::Stroke);
853+ paint.set_stroke_width(stroke.width);
854+ paint.set_stroke_join(join);
855+ paint.set_stroke_cap(cap);
856+ paint.set_stroke_miter(4.0);
857+}
858+
859+fn rgba_size(width: i32, height: i32) -> HostResult<usize> {
860+ if width <= 0 || height <= 0 {
861+ return Err(failure("canvas dimensions must be positive"));
862+ }
863+ (width as usize)
864+ .checked_mul(height as usize)
865+ .and_then(|size| size.checked_mul(4))
866+ .ok_or_else(|| failure("canvas dimensions overflow the RGBA buffer size"))
867+}
868+
869+fn font_size(value: &str) -> HostResult<f64> {
870+ let length = svgtypes::Length::from_str(value)?;
871+ let scale = match length.unit {
872+ svgtypes::LengthUnit::None | svgtypes::LengthUnit::Px => 1.0,
873+ svgtypes::LengthUnit::Pt => 96.0 / 72.0,
874+ svgtypes::LengthUnit::Pc => 16.0,
875+ svgtypes::LengthUnit::In => 96.0,
876+ svgtypes::LengthUnit::Cm => 96.0 / 2.54,
877+ svgtypes::LengthUnit::Mm => 96.0 / 25.4,
878+ _ => {
879+ return Err(failure(
880+ "native font measurements require an absolute CSS font size",
881+ ))
882+ }
883+ };
884+ let size = length.number * scale;
885+ if size > 0.0 && (size as f32).is_finite() && (size as f32) > 0.0 {
886+ Ok(size)
887+ } else {
888+ Err(failure(
889+ "font size must be positive and finite in native coordinates",
890+ ))
891+ }
892+}
native/runtime/src/lib.rsnew file+604
@@ -0,0 +1,604 @@
1+//! Synchronous native primitives for a bundled PureScript JavaScript program.
2+//! The host has no graph, document, command-line or application lifecycle policy.
3+
4+mod graphics;
5+mod process;
6+mod terminal;
7+
8+use std::cell::RefCell;
9+use std::collections::HashMap;
10+use std::error::Error;
11+use std::io::{self, IsTerminal, Read, Write};
12+use std::path::{Path, PathBuf};
13+use std::rc::Rc;
14+use std::sync::atomic::{AtomicBool, Ordering};
15+use std::sync::{Arc, LazyLock, Mutex};
16+use std::time::{Duration, Instant};
17+
18+use rquickjs::function::{Func, Rest};
19+use rquickjs::{
20+ Array, CaughtError, Context, Ctx, Exception, FromJs, Function, IntoJs, Object, Runtime, Value,
21+};
22+
23+use graphics::Graphics;
24+use process::Process;
25+use terminal::Terminal;
26+
27+pub struct FontAsset {
28+ pub family: &'static str,
29+ pub data: &'static [u8],
30+ pub weight: u16,
31+ pub italic: bool,
32+ /// Comma-separated HarfBuzz feature settings, e.g. `ss01=1,liga=0`.
33+ pub features: &'static str,
34+}
35+
36+pub struct Config {
37+ /// Arguments excluding the executable name.
38+ pub arguments: Vec<String>,
39+ pub version: String,
40+ pub fonts: Vec<FontAsset>,
41+ pub application: PathBuf,
42+ pub ffmpeg: PathBuf,
43+}
44+
45+type HostResult<T> = Result<T, Box<dyn Error>>;
46+
47+fn failure(message: impl Into<String>) -> Box<dyn Error> {
48+ io::Error::other(message.into()).into()
49+}
50+
51+struct Video {
52+ process: Process,
53+ pixels: Vec<u8>,
54+ width: i32,
55+ height: i32,
56+}
57+
58+struct Host {
59+ config: Config,
60+ graphics: Option<Graphics>,
61+ videos: HashMap<u32, Video>,
62+ next_video: u32,
63+ terminal: Terminal,
64+ started: Instant,
65+ interrupted: Arc<AtomicBool>,
66+}
67+
68+impl Host {
69+ fn graphics(&mut self) -> HostResult<&mut Graphics> {
70+ if self.graphics.is_none() {
71+ self.graphics = Some(Graphics::new(&self.config.fonts)?);
72+ }
73+ Ok(self.graphics.as_mut().expect("graphics initialized above"))
74+ }
75+
76+ fn begin_video(
77+ &mut self,
78+ output: String,
79+ fps: i32,
80+ width: i32,
81+ height: i32,
82+ ) -> HostResult<u32> {
83+ if fps <= 0 || width <= 0 || height <= 0 {
84+ return Err(failure("video fps, width and height must be positive"));
85+ }
86+ let next = self
87+ .next_video
88+ .checked_add(1)
89+ .ok_or_else(|| failure("video handle space exhausted"))?;
90+ let byte_length = (width as usize)
91+ .checked_mul(height as usize)
92+ .and_then(|size| size.checked_mul(4))
93+ .ok_or_else(|| failure("video frame dimensions are too large"))?;
94+ let mut pixels = Vec::new();
95+ pixels.try_reserve_exact(byte_length)?;
96+ pixels.resize(byte_length, 0);
97+ let arguments = vec![
98+ "-y".into(),
99+ "-loglevel".into(),
100+ "error".into(),
101+ "-nostdin".into(),
102+ "-f".into(),
103+ "rawvideo".into(),
104+ "-pixel_format".into(),
105+ "rgba".into(),
106+ "-video_size".into(),
107+ format!("{width}x{height}"),
108+ "-framerate".into(),
109+ fps.to_string(),
110+ "-i".into(),
111+ "pipe:0".into(),
112+ "-an".into(),
113+ "-c:v".into(),
114+ "libx264".into(),
115+ "-preset".into(),
116+ "medium".into(),
117+ "-crf".into(),
118+ "12".into(),
119+ "-tune".into(),
120+ "animation".into(),
121+ "-vf".into(),
122+ "pad=ceil(iw/2)*2:ceil(ih/2)*2".into(),
123+ "-pix_fmt".into(),
124+ "yuv420p".into(),
125+ "-movflags".into(),
126+ "+faststart".into(),
127+ output,
128+ ];
129+ let process = Process::spawn(&self.config.ffmpeg, &arguments, self.interrupted.clone())?;
130+ let id = self.next_video;
131+ self.next_video = next;
132+ self.videos.insert(
133+ id,
134+ Video {
135+ process,
136+ pixels,
137+ width,
138+ height,
139+ },
140+ );
141+ Ok(id)
142+ }
143+
144+ fn video_frame(&mut self, id: u32, surface: u32) -> HostResult<()> {
145+ // Remove while operating so any error drops/reaps the encoder at once.
146+ let mut video = self
147+ .videos
148+ .remove(&id)
149+ .ok_or_else(|| failure("video is not open"))?;
150+ self.graphics()?
151+ .read_rgba(surface, video.width, video.height, &mut video.pixels)?;
152+ video.pixels = video.process.write(std::mem::take(&mut video.pixels))?;
153+ self.videos.insert(id, video);
154+ Ok(())
155+ }
156+
157+ fn finish_video(&mut self, id: u32) -> HostResult<()> {
158+ let video = self
159+ .videos
160+ .remove(&id)
161+ .ok_or_else(|| failure("video is not open"))?;
162+ let result = video.process.finish()?;
163+ if !result.success {
164+ return Err(failure(format!(
165+ "FFmpeg exited with status {}:\n{}",
166+ result.code, result.stderr
167+ )));
168+ }
169+ Ok(())
170+ }
171+
172+ fn cancel_video(&mut self, id: u32) -> HostResult<()> {
173+ if let Some(video) = self.videos.remove(&id) {
174+ video.process.cancel()?;
175+ }
176+ Ok(())
177+ }
178+
179+ fn application(
180+ &self,
181+ args: Vec<String>,
182+ source: Option<String>,
183+ ) -> HostResult<process::Outcome> {
184+ process::run_application(
185+ &self.config.application,
186+ &args,
187+ source,
188+ self.interrupted.clone(),
189+ )
190+ }
191+
192+ fn sleep(&self, milliseconds: i32) -> HostResult<()> {
193+ if milliseconds < 0 {
194+ return Err(failure("sleep duration must not be negative"));
195+ }
196+ let deadline = Instant::now() + Duration::from_millis(milliseconds as u64);
197+ while let Some(remaining) = deadline.checked_duration_since(Instant::now()) {
198+ if self.interrupted.load(Ordering::Relaxed) {
199+ return Err(failure("interrupted"));
200+ }
201+ std::thread::sleep(remaining.min(Duration::from_millis(10)));
202+ }
203+ Ok(())
204+ }
205+
206+ fn shutdown(&mut self) -> HostResult<()> {
207+ let mut result = Ok(());
208+ for (_, video) in self.videos.drain() {
209+ let cancelled = video.process.cancel();
210+ if result.is_ok() {
211+ result = cancelled;
212+ }
213+ }
214+ let restored = self.terminal.restore();
215+ result.and(restored)
216+ }
217+}
218+
219+fn native<'js, T>(ctx: &Ctx<'js>, result: HostResult<T>) -> rquickjs::Result<T> {
220+ result.map_err(|error| Exception::throw_message(ctx, &error.to_string()))
221+}
222+
223+fn argument<'js, T: FromJs<'js>>(
224+ ctx: &Ctx<'js>,
225+ args: &[Value<'js>],
226+ index: usize,
227+) -> rquickjs::Result<T> {
228+ let value = args
229+ .get(index)
230+ .ok_or_else(|| Exception::throw_type(ctx, "missing native argument"))?;
231+ T::from_js(ctx, value.clone())
232+}
233+
234+fn canvas_color(value: Object<'_>) -> rquickjs::Result<graphics::Color> {
235+ Ok([
236+ value.get("r")?,
237+ value.get("g")?,
238+ value.get("b")?,
239+ value.get("a")?,
240+ ])
241+}
242+
243+fn canvas_affine(value: Object<'_>) -> rquickjs::Result<[f32; 6]> {
244+ Ok([
245+ value.get("a")?,
246+ value.get("b")?,
247+ value.get("c")?,
248+ value.get("d")?,
249+ value.get("e")?,
250+ value.get("f")?,
251+ ])
252+}
253+
254+fn canvas_stroke(value: Object<'_>) -> rquickjs::Result<graphics::Stroke> {
255+ Ok(graphics::Stroke {
256+ color: canvas_color(value.get("color")?)?,
257+ width: value.get("width")?,
258+ join: value.get("join")?,
259+ cap: value.get("cap")?,
260+ })
261+}
262+
263+fn canvas_path<'js>(ctx: &Ctx<'js>, value: Object<'js>) -> rquickjs::Result<skia_safe::Path> {
264+ let values: Array<'js> = value.get("values")?;
265+ let offset: i32 = value.get("offset")?;
266+ let length: i32 = value.get("length")?;
267+ let end = offset
268+ .checked_add(length)
269+ .filter(|&end| offset >= 0 && length >= 0 && (end as usize) <= values.len());
270+ let end = end.ok_or_else(|| Exception::throw_range(ctx, "invalid canvas path range"))?;
271+ native(
272+ ctx,
273+ graphics::path_from_values((offset as usize..end as usize).map(|index| {
274+ values
275+ .get::<f64>(index)
276+ .map_err(|error| failure(CaughtError::from_error(ctx, error).to_string()))
277+ })),
278+ )
279+}
280+
281+fn dispatch<'js>(
282+ ctx: &Ctx<'js>,
283+ host: &mut Host,
284+ name: &str,
285+ args: &[Value<'js>],
286+) -> rquickjs::Result<Value<'js>> {
287+ macro_rules! arg {
288+ ($index:expr, $type:ty) => {
289+ argument::<$type>(ctx, args, $index)?
290+ };
291+ }
292+ macro_rules! result {
293+ ($value:expr) => {
294+ native(ctx, $value)?.into_js(ctx)
295+ };
296+ }
297+ match name {
298+ "arguments" => host.config.arguments.as_slice().into_js(ctx),
299+ "version" => host.config.version.as_str().into_js(ctx),
300+ "isStdinTTY" => io::stdin().is_terminal().into_js(ctx),
301+ "readStdin" => result!((|| -> HostResult<String> {
302+ let mut source = String::new();
303+ io::stdin().read_to_string(&mut source)?;
304+ Ok(source)
305+ })()),
306+ "readText" => result!(std::fs::read_to_string(arg!(0, String)).map_err(Into::into)),
307+ "writeText" => {
308+ result!(std::fs::write(arg!(0, String), arg!(1, String)).map_err(Into::into))
309+ }
310+ "stdout" => result!(write_output(&mut io::stdout().lock(), &arg!(0, String))),
311+ "stderr" => result!(write_output(&mut io::stderr().lock(), &arg!(0, String))),
312+ "absolutePath" => {
313+ let path = arg!(0, String);
314+ let path = native(ctx, absolute_path(&path))?;
315+ path.into_js(ctx)
316+ }
317+ "environment" => std::env::var(arg!(0, String))
318+ .unwrap_or_default()
319+ .into_js(ctx),
320+ "monotonicMilliseconds" => (host.started.elapsed().as_secs_f64() * 1000.0).into_js(ctx),
321+ "runApplication" => {
322+ let outcome = native(
323+ ctx,
324+ host.application(arg!(0, Vec<String>), arg!(1, Option<String>)),
325+ )?;
326+ let value = Object::new(ctx.clone())?;
327+ value.set("success", outcome.success)?;
328+ value.set("code", outcome.code)?;
329+ value.set("stderr", outcome.stderr)?;
330+ value.into_js(ctx)
331+ }
332+ "measureText" => {
333+ let font = arg!(0, String);
334+ let text = arg!(1, String);
335+ let measured = native(
336+ ctx,
337+ host.graphics()
338+ .and_then(|graphics| graphics.measure(&font, &text)),
339+ )?;
340+ let value = Object::new(ctx.clone())?;
341+ value.set("width", measured.width)?;
342+ value.set("ascent", measured.ascent)?;
343+ value.set("descent", measured.descent)?;
344+ value.into_js(ctx)
345+ }
346+ "canvasCreate" => {
347+ let options = arg!(0, Object<'js>);
348+ let width = options.get("width")?;
349+ let height = options.get("height")?;
350+ result!(native(ctx, host.graphics())?.create(width, height))
351+ }
352+ "canvasRelease" => {
353+ result!(native(ctx, host.graphics())?.release(arg!(0, u32)))
354+ }
355+ "canvasReset" => {
356+ result!(native(ctx, host.graphics())?.reset(arg!(0, u32)))
357+ }
358+ "canvasClear" => {
359+ let id = arg!(0, u32);
360+ let color = canvas_color(arg!(1, Object<'js>))?;
361+ result!(native(ctx, host.graphics())?.clear(id, color))
362+ }
363+ "canvasSave" => {
364+ result!(native(ctx, host.graphics())?.save(arg!(0, u32)))
365+ }
366+ "canvasRestore" => {
367+ result!(native(ctx, host.graphics())?.restore(arg!(0, u32)))
368+ }
369+ "canvasSetTransform" => {
370+ let id = arg!(0, u32);
371+ let matrix = canvas_affine(arg!(1, Object<'js>))?;
372+ result!(native(ctx, host.graphics())?.set_transform(id, matrix))
373+ }
374+ "canvasTransform" => {
375+ let id = arg!(0, u32);
376+ let matrix = canvas_affine(arg!(1, Object<'js>))?;
377+ result!(native(ctx, host.graphics())?.transform(id, matrix))
378+ }
379+ "canvasClip" => {
380+ let id = arg!(0, u32);
381+ let path = canvas_path(ctx, arg!(1, Object<'js>))?;
382+ let even_odd = arg!(2, bool);
383+ result!(native(ctx, host.graphics())?.clip(id, &path, even_odd))
384+ }
385+ "canvasSetBlend" => {
386+ let id = arg!(0, u32);
387+ let mode = arg!(1, String);
388+ result!(native(ctx, host.graphics())?.set_blend(id, &mode))
389+ }
390+ "canvasSetBlur" => {
391+ let id = arg!(0, u32);
392+ let radius = arg!(1, f32);
393+ result!(native(ctx, host.graphics())?.set_blur(id, radius))
394+ }
395+ "canvasSetTint" => {
396+ let id = arg!(0, u32);
397+ let color = arg!(1, Option<Object<'js>>).map(canvas_color).transpose()?;
398+ result!(native(ctx, host.graphics())?.set_tint(id, color))
399+ }
400+ "canvasFillPath" => {
401+ let id = arg!(0, u32);
402+ let path = canvas_path(ctx, arg!(1, Object<'js>))?;
403+ let color = canvas_color(arg!(2, Object<'js>))?;
404+ result!(native(ctx, host.graphics())?.fill_path(id, &path, color))
405+ }
406+ "canvasStrokePath" => {
407+ let id = arg!(0, u32);
408+ let path = canvas_path(ctx, arg!(1, Object<'js>))?;
409+ let stroke = canvas_stroke(arg!(2, Object<'js>))?;
410+ result!(native(ctx, host.graphics())?.stroke_path(id, &path, stroke))
411+ }
412+ "canvasFillStrokePath" => {
413+ let id = arg!(0, u32);
414+ let path = canvas_path(ctx, arg!(1, Object<'js>))?;
415+ let color = canvas_color(arg!(2, Object<'js>))?;
416+ let stroke = canvas_stroke(arg!(3, Object<'js>))?;
417+ result!(native(ctx, host.graphics())?.fill_stroke_path(id, &path, color, stroke))
418+ }
419+ "canvasText" => {
420+ let id = arg!(0, u32);
421+ let options = arg!(1, Object<'js>);
422+ let font: String = options.get("font")?;
423+ let text: String = options.get("content")?;
424+ let x = options.get("x")?;
425+ let y = options.get("y")?;
426+ let color = canvas_color(options.get("color")?)?;
427+ let align = options.get("align")?;
428+ let baseline = options.get("baseline")?;
429+ result!(
430+ native(ctx, host.graphics())?.text(id, &font, &text, x, y, color, align, baseline)
431+ )
432+ }
433+ "canvasCirclePattern" => {
434+ let id = arg!(0, u32);
435+ let options = arg!(1, Object<'js>);
436+ let rect: Object<'js> = options.get("rect")?;
437+ let pattern = graphics::CirclePattern {
438+ rect: [
439+ rect.get("x")?,
440+ rect.get("y")?,
441+ rect.get("width")?,
442+ rect.get("height")?,
443+ ],
444+ background: canvas_color(options.get("background")?)?,
445+ ink: canvas_color(options.get("ink")?)?,
446+ tile: options.get("tile")?,
447+ radius: options.get("radius")?,
448+ origin: [options.get("originX")?, options.get("originY")?],
449+ };
450+ result!(native(ctx, host.graphics())?.circle_pattern(id, pattern))
451+ }
452+ "canvasSurface" => {
453+ let destination = arg!(0, u32);
454+ let source = arg!(1, u32);
455+ result!(native(ctx, host.graphics())?.surface(destination, source))
456+ }
457+ "canvasWritePng" => {
458+ let id = arg!(0, u32);
459+ let path = arg!(1, String);
460+ result!(native(ctx, host.graphics())?.png(id, &path))
461+ }
462+ "beginVideo" => {
463+ let options = arg!(0, Object<'js>);
464+ result!(host.begin_video(
465+ options.get("output")?,
466+ options.get("fps")?,
467+ options.get("width")?,
468+ options.get("height")?
469+ ))
470+ }
471+ "writeVideoFrame" => result!(host.video_frame(arg!(0, u32), arg!(1, u32))),
472+ "finishVideo" => result!(host.finish_video(arg!(0, u32))),
473+ "cancelVideo" => result!(host.cancel_video(arg!(0, u32))),
474+ "enterTerminal" => result!(host.terminal.enter()),
475+ "restoreTerminal" => result!(host.terminal.restore()),
476+ "terminalSize" => {
477+ let (width, height) = if io::stdin().is_terminal() || io::stdout().is_terminal() {
478+ native(ctx, crossterm::terminal::size().map_err(Into::into))?
479+ } else {
480+ (0, 0)
481+ };
482+ let value = Object::new(ctx.clone())?;
483+ value.set("width", width)?;
484+ value.set("height", height)?;
485+ value.into_js(ctx)
486+ }
487+ "readKey" => result!(host.terminal.key()),
488+ "sleepMilliseconds" => result!(host.sleep(arg!(0, i32))),
489+ _ => Err(Exception::throw_type(
490+ ctx,
491+ &format!("unknown native primitive: {name}"),
492+ )),
493+ }
494+}
495+
496+fn write_output(output: &mut impl Write, text: &str) -> HostResult<()> {
497+ output.write_all(text.as_bytes())?;
498+ output.flush()?;
499+ Ok(())
500+}
501+
502+fn absolute_path(path: &str) -> HostResult<String> {
503+ let path = Path::new(path);
504+ // Unlike canonicalize, absolute() works for an output that does not exist.
505+ let absolute = std::path::absolute(path)?;
506+ absolute
507+ .into_os_string()
508+ .into_string()
509+ .map_err(|_| failure("absolute path is not UTF-8"))
510+}
511+
512+static RUN_LOCK: Mutex<()> = Mutex::new(());
513+static INTERRUPT: LazyLock<Result<Arc<AtomicBool>, String>> = LazyLock::new(|| {
514+ let flag = Arc::new(AtomicBool::new(false));
515+ let handler_flag = flag.clone();
516+ ctrlc::set_handler(move || {
517+ handler_flag.store(true, Ordering::Relaxed);
518+ })
519+ .map_err(|error| error.to_string())?;
520+ Ok(flag)
521+});
522+
523+fn register<'js>(ctx: &Ctx<'js>, state: Rc<RefCell<Host>>) -> rquickjs::Result<()> {
524+ let host = Object::new(ctx.clone())?;
525+ host.set(
526+ "call",
527+ Func::from(
528+ move |ctx: Ctx<'js>,
529+ name: String,
530+ args: Rest<Value<'js>>|
531+ -> rquickjs::Result<Value<'js>> {
532+ let mut state = state.try_borrow_mut().map_err(|_| {
533+ Exception::throw_message(&ctx, "native host cannot be called recursively")
534+ })?;
535+ dispatch(&ctx, &mut state, &name, &args.0)
536+ },
537+ ),
538+ )?;
539+ ctx.globals().set("__purescriptNativeHost", host)
540+}
541+
542+/// Execute a synchronous IIFE whose `globalThis.__purescriptMain()` returns an
543+/// integer exit status. Native exceptions become ordinary JS Error instances.
544+/// Terminal state and child processes are released on success, JS errors and
545+/// Rust unwinding. Only one terminal-owning runtime may run in a process at once.
546+pub fn run(script: &str, config: Config) -> Result<i32, Box<dyn Error>> {
547+ let _guard = RUN_LOCK
548+ .lock()
549+ .map_err(|_| failure("native runtime lock is poisoned"))?;
550+ let interrupted = INTERRUPT
551+ .as_ref()
552+ .map_err(|error| failure(error.clone()))?
553+ .clone();
554+ interrupted.store(false, Ordering::Relaxed);
555+ let state = Rc::new(RefCell::new(Host {
556+ config,
557+ graphics: None,
558+ videos: HashMap::new(),
559+ next_video: 1,
560+ terminal: Terminal::default(),
561+ started: Instant::now(),
562+ interrupted: interrupted.clone(),
563+ }));
564+ let runtime = Runtime::new()?;
565+ let interrupt_flag = interrupted.clone();
566+ runtime.set_interrupt_handler(Some(Box::new(move || {
567+ interrupt_flag.load(Ordering::Relaxed)
568+ })));
569+ // Native stack allowance is independent of JS heap size; generated code can
570+ // recurse through real document trees without inheriting QuickJS's tiny default.
571+ runtime.set_max_stack_size(8 * 1024 * 1024);
572+ let context = Context::full(&runtime)?;
573+ let outcome = context.with(|ctx| -> HostResult<i32> {
574+ let execute = || -> rquickjs::Result<i32> {
575+ register(&ctx, state.clone())?;
576+ ctx.eval::<(), _>(script)?;
577+ let main: Function = ctx.globals().get("__purescriptMain")?;
578+ let status: f64 = main.call(())?;
579+ if !status.is_finite()
580+ || status.fract() != 0.0
581+ || status < i32::MIN as f64
582+ || status > i32::MAX as f64
583+ {
584+ return Err(Exception::throw_type(
585+ &ctx,
586+ "__purescriptMain must return an integer exit status",
587+ ));
588+ }
589+ Ok(status as i32)
590+ };
591+ execute().map_err(|error| failure(CaughtError::from_error(&ctx, error).to_string()))
592+ });
593+ let cleanup = state.borrow_mut().shutdown();
594+ // Drop the context before the runtime, never retain a JS value in native
595+ // state, and never let a callback outlive the engine that created it.
596+ drop(context);
597+ drop(runtime);
598+ cleanup?;
599+ if interrupted.load(Ordering::Relaxed) {
600+ Ok(130)
601+ } else {
602+ outcome
603+ }
604+}
native/runtime/src/process.rsnew file+331
@@ -0,0 +1,331 @@
1+use std::io::{self, Read, Write};
2+use std::path::Path;
3+use std::process::{Child, Command, ExitStatus, Stdio};
4+use std::sync::atomic::{AtomicBool, Ordering};
5+use std::sync::{mpsc, Arc};
6+use std::thread::{self, JoinHandle};
7+use std::time::Duration;
8+
9+use crate::{failure, HostResult};
10+
11+const POLL: Duration = Duration::from_millis(10);
12+
13+type Written = (Vec<u8>, io::Result<()>);
14+
15+/// Owns an encoder's pipes, threads and process group. The single in-flight
16+/// write returns its allocation so the next frame can reuse its pixel buffer.
17+pub(crate) struct Process {
18+ child: Child,
19+ input: Option<mpsc::SyncSender<Vec<u8>>>,
20+ written: mpsc::Receiver<Written>,
21+ writer: Option<JoinHandle<()>>,
22+ reader: Option<JoinHandle<io::Result<Vec<u8>>>>,
23+ interrupted: Arc<AtomicBool>,
24+ status: Option<ExitStatus>,
25+}
26+
27+pub(crate) struct Outcome {
28+ pub success: bool,
29+ pub code: i32,
30+ pub stderr: String,
31+}
32+
33+/// An application owns its lifetime, unlike an encoder. The worker keeps
34+/// delivering source and reaps the child even after its caller stops waiting;
35+/// neither an interrupt nor an I/O error sends a signal to the application.
36+pub(crate) fn run_application(
37+ executable: &Path,
38+ arguments: &[String],
39+ source: Option<String>,
40+ interrupted: Arc<AtomicBool>,
41+) -> HostResult<Outcome> {
42+ if interrupted.load(Ordering::Relaxed) {
43+ return Err(failure("interrupted"));
44+ }
45+ let mut command = Command::new(executable);
46+ command
47+ .args(arguments)
48+ .stdin(if source.is_some() {
49+ Stdio::piped()
50+ } else {
51+ Stdio::null()
52+ })
53+ .stdout(Stdio::inherit())
54+ .stderr(Stdio::inherit());
55+ #[cfg(unix)]
56+ {
57+ use std::os::unix::process::CommandExt;
58+ command.process_group(0);
59+ }
60+ #[cfg(windows)]
61+ {
62+ use std::os::windows::process::CommandExt;
63+ // CREATE_NEW_PROCESS_GROUP disables inherited console CTRL_C events.
64+ command.creation_flags(0x0000_0200);
65+ }
66+ let executable = executable.display().to_string();
67+ let (complete, outcome) = mpsc::channel();
68+ let launch_interrupted = interrupted.clone();
69+ // Start the owner before launching the application: thread-creation failure
70+ // must not leave a child without a waiter. Everything moved here is owned.
71+ thread::Builder::new()
72+ .name("native-application".into())
73+ .spawn(move || {
74+ let report = |result: Result<Outcome, String>| {
75+ // Also report errors after the caller has detached. Always print
76+ // errors, avoiding a send/interrupt race that could hide a failure.
77+ if let Err(message) = &result {
78+ let _ = writeln!(io::stderr().lock(), "{message}");
79+ }
80+ let _ = complete.send(result);
81+ };
82+ if launch_interrupted.load(Ordering::Relaxed) {
83+ report(Err("interrupted".into()));
84+ return;
85+ }
86+ let mut child = match command.spawn() {
87+ Ok(child) => child,
88+ Err(error) => {
89+ report(Err(format!("{executable}: {error}")));
90+ return;
91+ }
92+ };
93+ let mut input_failed = false;
94+ if let Some(source) = source {
95+ // Output is inherited, not captured: writing stdin cannot deadlock
96+ // against a full stdout/stderr pipe. A blocked write remains on this
97+ // worker, so the invoking CLI can still respond to interruption.
98+ let delivery = match child.stdin.take() {
99+ Some(mut stdin) => stdin.write_all(source.as_bytes()),
100+ None => Err(io::Error::other("application stdin pipe is unavailable")),
101+ };
102+ // stdin has closed (EOF) before waiting, including on write failure.
103+ if let Err(error) = delivery {
104+ input_failed = true;
105+ report(Err(format!("writing {executable} stdin: {error}")));
106+ }
107+ }
108+ // A delivery error releases the caller immediately, but this worker
109+ // still waits without terminating an application with editable state.
110+ match child.wait() {
111+ Ok(status) if !input_failed => report(Ok(Outcome {
112+ success: status.success(),
113+ code: status.code().unwrap_or(-1),
114+ stderr: String::new(),
115+ })),
116+ Ok(_) => {}
117+ Err(error) => report(Err(format!("waiting for {executable}: {error}"))),
118+ }
119+ })?;
120+ loop {
121+ match outcome.recv_timeout(POLL) {
122+ Ok(result) => return result.map_err(failure),
123+ Err(mpsc::RecvTimeoutError::Disconnected) => {
124+ return Err(failure("application waiter stopped"))
125+ }
126+ Err(mpsc::RecvTimeoutError::Timeout) => {
127+ if interrupted.load(Ordering::Relaxed) {
128+ return Err(failure("interrupted"));
129+ }
130+ }
131+ }
132+ }
133+}
134+
135+impl Process {
136+ pub fn spawn(
137+ executable: &Path,
138+ arguments: &[String],
139+ interrupted: Arc<AtomicBool>,
140+ ) -> HostResult<Self> {
141+ let mut command = Command::new(executable);
142+ command
143+ .args(arguments)
144+ .stdin(Stdio::piped())
145+ .stdout(Stdio::null())
146+ .stderr(Stdio::piped());
147+ // A separate process group prevents a terminal SIGINT from killing the
148+ // child before we can close pipes, reap it and restore the terminal.
149+ #[cfg(unix)]
150+ {
151+ use std::os::unix::process::CommandExt;
152+ command.process_group(0);
153+ }
154+ let child = command
155+ .spawn()
156+ .map_err(|e| failure(format!("{}: {e}", executable.display())))?;
157+ let (input, requests) = mpsc::sync_channel::<Vec<u8>>(1);
158+ let (complete, written) = mpsc::sync_channel(1);
159+ let mut process = Self {
160+ child,
161+ input: None,
162+ written,
163+ writer: None,
164+ reader: None,
165+ interrupted,
166+ status: None,
167+ };
168+ let mut stderr = process
169+ .child
170+ .stderr
171+ .take()
172+ .ok_or_else(|| failure("child stderr pipe is unavailable"))?;
173+ process.reader = Some(thread::Builder::new().name("native-stderr".into()).spawn(
174+ move || {
175+ let mut bytes = Vec::new();
176+ stderr.read_to_end(&mut bytes)?;
177+ Ok(bytes)
178+ },
179+ )?);
180+ let mut stdin = process
181+ .child
182+ .stdin
183+ .take()
184+ .ok_or_else(|| failure("child stdin pipe is unavailable"))?;
185+ process.writer = Some(thread::Builder::new().name("native-stdin".into()).spawn(
186+ move || {
187+ while let Ok(bytes) = requests.recv() {
188+ let result = stdin.write_all(&bytes);
189+ let failed = result.is_err();
190+ if complete.send((bytes, result)).is_err() || failed {
191+ break;
192+ }
193+ }
194+ // Dropping stdin delivers EOF, including on a write failure.
195+ },
196+ )?);
197+ process.input = Some(input);
198+ Ok(process)
199+ }
200+
201+ pub fn write(&mut self, bytes: Vec<u8>) -> HostResult<Vec<u8>> {
202+ self.check_interrupt()?;
203+ let input = self
204+ .input
205+ .as_ref()
206+ .ok_or_else(|| failure("child input is closed"))?;
207+ if input.send(bytes).is_err() {
208+ return Err(self.write_error("child input writer stopped"));
209+ }
210+ loop {
211+ match self.written.recv_timeout(POLL) {
212+ Ok((bytes, Ok(()))) => return Ok(bytes),
213+ Ok((_, Err(error))) => return Err(self.write_error(&error.to_string())),
214+ Err(mpsc::RecvTimeoutError::Disconnected) => {
215+ return Err(self.write_error("child input writer stopped"))
216+ }
217+ Err(mpsc::RecvTimeoutError::Timeout) => self.check_interrupt()?,
218+ }
219+ }
220+ }
221+
222+ fn write_error(&mut self, message: &str) -> Box<dyn std::error::Error> {
223+ self.terminate();
224+ match self.take_stderr() {
225+ Ok(stderr) => failure(format!("writing child stdin: {message}\n{stderr}")),
226+ Err(error) => failure(format!(
227+ "writing child stdin: {message}; reading stderr: {error}"
228+ )),
229+ }
230+ }
231+
232+ fn check_interrupt(&self) -> HostResult<()> {
233+ if self.interrupted.load(Ordering::Relaxed) {
234+ Err(failure("interrupted"))
235+ } else {
236+ Ok(())
237+ }
238+ }
239+
240+ fn close_input(&mut self) -> HostResult<()> {
241+ self.input.take();
242+ if let Some(writer) = self.writer.take() {
243+ writer
244+ .join()
245+ .map_err(|_| failure("child stdin writer panicked"))?;
246+ }
247+ Ok(())
248+ }
249+
250+ pub fn finish(mut self) -> HostResult<Outcome> {
251+ self.close_input()?;
252+ let status = loop {
253+ self.check_interrupt()?;
254+ if let Some(status) = self.child.try_wait()? {
255+ self.status = Some(status);
256+ break status;
257+ }
258+ thread::sleep(POLL);
259+ };
260+ // Reap/close the child's remaining process group before joining readers:
261+ // descendants must not retain our pipe after the owning child exits.
262+ self.kill_group();
263+ let stderr = self.take_stderr()?;
264+ Ok(Outcome {
265+ success: status.success(),
266+ code: status.code().unwrap_or(-1),
267+ stderr,
268+ })
269+ }
270+
271+ pub fn cancel(mut self) -> HostResult<()> {
272+ self.input.take();
273+ self.kill_group();
274+ if self.status.is_none() {
275+ self.status = Some(self.child.wait()?);
276+ }
277+ self.close_input()?;
278+ self.take_stderr()?;
279+ Ok(())
280+ }
281+
282+ fn take_stderr(&mut self) -> HostResult<String> {
283+ match self.reader.take() {
284+ Some(reader) => {
285+ let bytes = reader
286+ .join()
287+ .map_err(|_| failure("child stderr reader panicked"))??;
288+ Ok(String::from_utf8_lossy(&bytes).into_owned())
289+ }
290+ None => Ok(String::new()),
291+ }
292+ }
293+
294+ fn kill_group(&mut self) {
295+ #[cfg(unix)]
296+ {
297+ // The group ID was created by Command::process_group(0) for this
298+ // owned child. Never signal the parent's terminal process group.
299+ unsafe {
300+ libc::kill(-(self.child.id() as i32), libc::SIGKILL);
301+ }
302+ }
303+ if self.status.is_none() {
304+ let _ = self.child.kill();
305+ }
306+ }
307+
308+ fn terminate(&mut self) {
309+ self.input.take();
310+ self.kill_group();
311+ if self.status.is_none() {
312+ self.status = self.child.wait().ok();
313+ }
314+ if let Some(writer) = self.writer.take() {
315+ let _ = writer.join();
316+ }
317+ }
318+}
319+
320+impl Drop for Process {
321+ fn drop(&mut self) {
322+ // Only terminate a live ownership scope. finish() has already joined
323+ // both threads; avoid sending signals to a potentially reused PID.
324+ if self.writer.is_some() || self.reader.is_some() || self.status.is_none() {
325+ self.terminate();
326+ if let Some(reader) = self.reader.take() {
327+ let _ = reader.join();
328+ }
329+ }
330+ }
331+}
native/runtime/src/terminal.rsnew file+281
@@ -0,0 +1,281 @@
1+use std::io::{self, IsTerminal, Write};
2+use std::time::Duration;
3+#[cfg(unix)]
4+use std::time::Instant;
5+
6+use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
7+use crossterm::terminal::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled};
8+
9+use crate::{failure, HostResult};
10+
11+#[derive(Default)]
12+pub(crate) struct Terminal {
13+ active: bool,
14+ raw_enabled: bool,
15+ #[cfg(unix)]
16+ pipe: Option<PipeInput>,
17+}
18+
19+impl Terminal {
20+ pub fn enter(&mut self) -> HostResult<()> {
21+ if !self.active {
22+ if io::stdin().is_terminal() {
23+ // Leave raw mode alone when it belongs to the invoking host.
24+ if !is_raw_mode_enabled()? {
25+ enable_raw_mode()?;
26+ self.raw_enabled = true;
27+ }
28+ } else {
29+ #[cfg(unix)]
30+ {
31+ self.pipe = Some(PipeInput::open()?);
32+ }
33+ #[cfg(not(unix))]
34+ return Err(failure("non-TTY terminal input is supported only on Unix"));
35+ }
36+ self.active = true;
37+ }
38+ Ok(())
39+ }
40+
41+ pub fn restore(&mut self) -> HostResult<()> {
42+ if self.active {
43+ // Do not mark restored until raw mode was actually disabled: Drop
44+ // must retry if this operation fails while unwinding JS effects.
45+ if self.raw_enabled {
46+ disable_raw_mode()?;
47+ self.raw_enabled = false;
48+ }
49+ #[cfg(unix)]
50+ if let Some(pipe) = &mut self.pipe {
51+ pipe.restore()?;
52+ self.pipe = None;
53+ }
54+ let mut output = io::stdout().lock();
55+ output.write_all(b"\x1b[0m\x1b[?25h\x1b[?7h")?;
56+ output.flush()?;
57+ self.active = false;
58+ }
59+ Ok(())
60+ }
61+
62+ pub fn key(&mut self) -> HostResult<String> {
63+ if !self.active {
64+ return Err(failure("terminal input has not been entered"));
65+ }
66+ #[cfg(unix)]
67+ if let Some(pipe) = &mut self.pipe {
68+ return pipe.key().map_err(Into::into);
69+ }
70+ // Return one protocol token, not a batch of unrelated keystrokes.
71+ while event::poll(Duration::ZERO)? {
72+ if let Event::Key(key) = event::read()? {
73+ if key.kind == KeyEventKind::Release {
74+ continue;
75+ }
76+ let token = match key.code {
77+ KeyCode::Char(c) if key.modifiers.contains(KeyModifiers::CONTROL) => {
78+ let c = c.to_ascii_lowercase();
79+ if c.is_ascii_lowercase() {
80+ char::from((c as u8 - b'a') + 1).to_string()
81+ } else {
82+ c.to_string()
83+ }
84+ }
85+ KeyCode::Char(c) => c.to_string(),
86+ KeyCode::Esc => "\x1b".into(),
87+ KeyCode::Up => "\x1b[A".into(),
88+ KeyCode::Down => "\x1b[B".into(),
89+ KeyCode::Right => "\x1b[C".into(),
90+ KeyCode::Left => "\x1b[D".into(),
91+ KeyCode::Enter => "\r".into(),
92+ KeyCode::Tab => "\t".into(),
93+ KeyCode::Backspace => "\x7f".into(),
94+ KeyCode::Home => "\x1b[H".into(),
95+ KeyCode::End => "\x1b[F".into(),
96+ KeyCode::Delete => "\x1b[3~".into(),
97+ KeyCode::Insert => "\x1b[2~".into(),
98+ KeyCode::PageUp => "\x1b[5~".into(),
99+ KeyCode::PageDown => "\x1b[6~".into(),
100+ _ => continue,
101+ };
102+ return Ok(token);
103+ }
104+ }
105+ Ok(String::new())
106+ }
107+}
108+
109+impl Drop for Terminal {
110+ fn drop(&mut self) {
111+ let _ = self.restore();
112+ }
113+}
114+
115+#[cfg(unix)]
116+struct PipeInput {
117+ file: std::fs::File,
118+ original_flags: Option<i32>,
119+ bytes: Vec<u8>,
120+ offset: usize,
121+ eof: bool,
122+ escape_since: Option<Instant>,
123+}
124+
125+#[cfg(unix)]
126+impl PipeInput {
127+ fn open() -> io::Result<Self> {
128+ use std::os::fd::{AsRawFd, FromRawFd};
129+ // A duplicate keeps stdin open for this input session. File-status
130+ // flags are shared with stdin, so restore our change before closing it.
131+ let fd = unsafe { libc::fcntl(io::stdin().as_raw_fd(), libc::F_DUPFD_CLOEXEC, 0) };
132+ if fd < 0 {
133+ return Err(io::Error::last_os_error());
134+ }
135+ let file = unsafe { std::fs::File::from_raw_fd(fd) };
136+ let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
137+ if flags < 0 {
138+ return Err(io::Error::last_os_error());
139+ }
140+ let original_flags = if flags & libc::O_NONBLOCK == 0 {
141+ if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
142+ return Err(io::Error::last_os_error());
143+ }
144+ Some(flags)
145+ } else {
146+ None
147+ };
148+ Ok(Self {
149+ file,
150+ original_flags,
151+ bytes: Vec::new(),
152+ offset: 0,
153+ eof: false,
154+ escape_since: None,
155+ })
156+ }
157+
158+ fn restore(&mut self) -> io::Result<()> {
159+ use std::os::fd::AsRawFd;
160+ if let Some(flags) = self.original_flags {
161+ if unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_SETFL, flags) } < 0 {
162+ return Err(io::Error::last_os_error());
163+ }
164+ self.original_flags = None;
165+ }
166+ Ok(())
167+ }
168+
169+ fn key(&mut self) -> io::Result<String> {
170+ use std::io::Read;
171+ if let Some(token) = self.token(false) {
172+ return Ok(token);
173+ }
174+ if !self.eof {
175+ let mut chunk = [0; 1024];
176+ match self.file.read(&mut chunk) {
177+ Ok(0) => self.eof = true,
178+ Ok(count) => {
179+ if self.offset != 0 {
180+ self.bytes.drain(..self.offset);
181+ self.offset = 0;
182+ }
183+ self.bytes.extend_from_slice(&chunk[..count]);
184+ }
185+ Err(error)
186+ if matches!(
187+ error.kind(),
188+ io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
189+ ) => {}
190+ Err(error) => return Err(error),
191+ }
192+ }
193+ Ok(self.token(true).unwrap_or_default())
194+ }
195+
196+ fn token(&mut self, allow_escape_timeout: bool) -> Option<String> {
197+ let bytes = &self.bytes[self.offset..];
198+ let first = *bytes.first()?;
199+ let (count, token) = if first == 0x1b {
200+ // An escape may arrive separately from the rest of an ANSI key.
201+ // A lone escape must still become available while a pipe is open.
202+ let since = self.escape_since.get_or_insert_with(Instant::now);
203+ let complete = if matches!(bytes.get(1), Some(b'[' | b'O')) {
204+ bytes[2..]
205+ .iter()
206+ .position(|byte| !(0x20..=0x3f).contains(byte))
207+ .filter(|index| (0x40..=0x7e).contains(&bytes[index + 2]))
208+ .map(|index| index + 3)
209+ } else {
210+ None
211+ };
212+ if let Some(count) = complete {
213+ let sequence = &bytes[..count];
214+ let canonical = match sequence[count - 1] {
215+ b'A' => "\x1b[A",
216+ b'B' => "\x1b[B",
217+ b'C' => "\x1b[C",
218+ b'D' => "\x1b[D",
219+ b'H' => "\x1b[H",
220+ b'F' => "\x1b[F",
221+ _ if matches!(sequence, b"\x1b[1~" | b"\x1b[7~") => "\x1b[H",
222+ _ if matches!(sequence, b"\x1b[4~" | b"\x1b[8~") => "\x1b[F",
223+ _ => "",
224+ };
225+ (
226+ count,
227+ if canonical.is_empty() {
228+ String::from_utf8_lossy(sequence).into_owned()
229+ } else {
230+ canonical.to_owned()
231+ },
232+ )
233+ } else {
234+ let incomplete = bytes.len() == 1
235+ || (matches!(bytes.get(1), Some(b'[' | b'O'))
236+ && bytes[2..].iter().all(|byte| (0x20..=0x3f).contains(byte)));
237+ if incomplete
238+ && !self.eof
239+ && bytes.len() < 64
240+ && (!allow_escape_timeout || since.elapsed() < Duration::from_millis(25))
241+ {
242+ return None;
243+ }
244+ (1, "\x1b".to_owned())
245+ }
246+ } else {
247+ let width = match first {
248+ 0x00..=0x7f => 1,
249+ 0xc2..=0xdf => 2,
250+ 0xe0..=0xef => 3,
251+ 0xf0..=0xf4 => 4,
252+ _ => 1,
253+ };
254+ let available = &bytes[..width.min(bytes.len())];
255+ match std::str::from_utf8(available) {
256+ Ok("\n") => (1, "\r".to_owned()),
257+ Ok("\x08") => (1, "\x7f".to_owned()),
258+ Ok(text) => (width, text.to_owned()),
259+ Err(error) if error.error_len().is_none() && !self.eof => return None,
260+ Err(error) => (
261+ error.error_len().unwrap_or(available.len()),
262+ "\u{fffd}".to_owned(),
263+ ),
264+ }
265+ };
266+ self.offset += count;
267+ self.escape_since = None;
268+ if self.offset == self.bytes.len() {
269+ self.bytes.clear();
270+ self.offset = 0;
271+ }
272+ Some(token)
273+ }
274+}
275+
276+#[cfg(unix)]
277+impl Drop for PipeInput {
278+ fn drop(&mut self) {
279+ let _ = self.restore();
280+ }
281+}
src/Tauri/Launch.js+2
@@ -4,3 +4,5 @@ export const argumentsImpl = () => invoke("plugin:launch-file|arguments");
44
55export const authorizeFileArgumentImpl = path => () =>
66 invoke("plugin:launch-file|authorize_file_argument", { path });
7+
8+export const readStdinImpl = () => invoke("plugin:launch-file|read_stdin");
src/Tauri/Launch.purs+11−4
@@ -1,8 +1,9 @@
1−-- | Process startup arguments and file access explicitly authorized by them.
1+-- | Process startup arguments, standard input, and explicitly authorized files.
22-- | Register tauri-plugin-launch-file alongside tauri-plugin-fs and grant
3−-- | launch-file:allow-arguments and launch-file:allow-authorize-file-argument
4−-- | only to the application webview that handles launch arguments.
5−module Tauri.Launch (arguments, authorizeFileArgument) where
3+-- | launch-file:allow-arguments, launch-file:allow-authorize-file-argument,
4+-- | and launch-file:allow-read-stdin only to the application webview that
5+-- | handles launch input.
6+module Tauri.Launch (arguments, authorizeFileArgument, readStdin) where
67
78import Effect (Effect)
89import Effect.Aff (Aff)
@@ -20,5 +21,11 @@ arguments = toAffE argumentsImpl
2021authorizeFileArgument :: String -> Aff String
2122authorizeFileArgument path = toAffE (authorizeFileArgumentImpl path)
2223
24+-- | Read UTF-8 standard input to EOF on a native blocking worker.
25+-- | I/O and invalid UTF-8 errors reject the Aff.
26+readStdin :: Aff String
27+readStdin = toAffE readStdinImpl
28+
2329foreign import argumentsImpl :: Effect (Promise (Array String))
2430foreign import authorizeFileArgumentImpl :: String -> Effect (Promise String)
31+foreign import readStdinImpl :: Effect (Promise String)
src/Tauri/Native.jsnew file+29
@@ -0,0 +1,29 @@
1+const host = () => {
2+ const native = globalThis.__purescriptNativeHost;
3+ if (!native) throw new Error("Tauri.Native requires tauri-native-host");
4+ return native;
5+};
6+
7+const nativeArguments = () => host().call("arguments");
8+export { nativeArguments as arguments };
9+export const version = () => host().call("version");
10+export const isStdinTTY = () => host().call("isStdinTTY");
11+export const readStdin = () => host().call("readStdin");
12+export const readText = path => () => host().call("readText", path);
13+export const writeText = path => text => () => { host().call("writeText", path, text); };
14+export const stdout = text => () => { host().call("stdout", text); };
15+export const stderr = text => () => { host().call("stderr", text); };
16+export const absolutePath = path => () => host().call("absolutePath", path);
17+export const environment = name => () => host().call("environment", name);
18+export const monotonicMilliseconds = () => host().call("monotonicMilliseconds");
19+export const runApplicationImpl = args => input => () => host().call("runApplication", args, input);
20+export const measureText = font => text => () => host().call("measureText", font, text);
21+export const beginVideo = options => () => host().call("beginVideo", options);
22+export const writeVideoFrame = video => surface => () => { host().call("writeVideoFrame", video, surface); };
23+export const finishVideo = video => () => { host().call("finishVideo", video); };
24+export const cancelVideo = video => () => { host().call("cancelVideo", video); };
25+export const enterTerminal = () => { host().call("enterTerminal"); };
26+export const restoreTerminal = () => { host().call("restoreTerminal"); };
27+export const terminalSize = () => host().call("terminalSize");
28+export const readKey = () => host().call("readKey");
29+export const sleepMilliseconds = milliseconds => () => { host().call("sleepMilliseconds", milliseconds); };
src/Tauri/Native.pursnew file+71
@@ -0,0 +1,71 @@
1+-- | Synchronous native primitives provided by tauri-native-host. This module
2+-- | requires the embedded native host; it does not emulate browser or Node APIs.
3+module Tauri.Native
4+ ( Metrics
5+ , ProcessResult
6+ , Video
7+ , arguments
8+ , version
9+ , isStdinTTY
10+ , readStdin
11+ , readText
12+ , writeText
13+ , stdout
14+ , stderr
15+ , absolutePath
16+ , environment
17+ , monotonicMilliseconds
18+ , runApplication
19+ , measureText
20+ , beginVideo
21+ , writeVideoFrame
22+ , finishVideo
23+ , cancelVideo
24+ , enterTerminal
25+ , restoreTerminal
26+ , terminalSize
27+ , readKey
28+ , sleepMilliseconds
29+ ) where
30+
31+import Prelude
32+
33+import Data.Maybe (Maybe)
34+import Data.Nullable (Nullable, toNullable)
35+import Effect (Effect)
36+import Tauri.Native.Canvas (Surface)
37+
38+type Metrics = { width :: Number, ascent :: Number, descent :: Number }
39+
40+type ProcessResult = { success :: Boolean, code :: Int, stderr :: String }
41+
42+-- | Launch the configured sibling application with literal arguments. Source
43+-- | data, when present, is sent through stdin and never becomes an argument.
44+-- | Output is inherited; interrupting this wait does not terminate the app.
45+runApplication :: Array String -> Maybe String -> Effect ProcessResult
46+runApplication args input = runApplicationImpl args (toNullable input)
47+
48+foreign import data Video :: Type
49+
50+foreign import arguments :: Effect (Array String)
51+foreign import version :: Effect String
52+foreign import isStdinTTY :: Effect Boolean
53+foreign import readStdin :: Effect String
54+foreign import readText :: String -> Effect String
55+foreign import writeText :: String -> String -> Effect Unit
56+foreign import stdout :: String -> Effect Unit
57+foreign import stderr :: String -> Effect Unit
58+foreign import absolutePath :: String -> Effect String
59+foreign import environment :: String -> Effect String
60+foreign import monotonicMilliseconds :: Effect Number
61+foreign import runApplicationImpl :: Array String -> Nullable String -> Effect ProcessResult
62+foreign import measureText :: String -> String -> Effect Metrics
63+foreign import beginVideo :: { output :: String, fps :: Int, width :: Int, height :: Int } -> Effect Video
64+foreign import writeVideoFrame :: Video -> Surface -> Effect Unit
65+foreign import finishVideo :: Video -> Effect Unit
66+foreign import cancelVideo :: Video -> Effect Unit
67+foreign import enterTerminal :: Effect Unit
68+foreign import restoreTerminal :: Effect Unit
69+foreign import terminalSize :: Effect { width :: Int, height :: Int }
70+foreign import readKey :: Effect String
71+foreign import sleepMilliseconds :: Int -> Effect Unit
src/Tauri/Native/Canvas.jsnew file+25
@@ -0,0 +1,25 @@
1+const host = () => {
2+ const native = globalThis.__purescriptNativeHost;
3+ if (!native) throw new Error("Tauri.Native.Canvas requires tauri-native-host");
4+ return native;
5+};
6+
7+export const create = size => () => host().call("canvasCreate", size);
8+export const release = target => () => { host().call("canvasRelease", target); };
9+export const reset = target => () => { host().call("canvasReset", target); };
10+export const clear = target => color => () => { host().call("canvasClear", target, color); };
11+export const save = target => () => { host().call("canvasSave", target); };
12+export const restore = target => () => { host().call("canvasRestore", target); };
13+export const setTransform = target => affine => () => { host().call("canvasSetTransform", target, affine); };
14+export const transform = target => affine => () => { host().call("canvasTransform", target, affine); };
15+export const clip = target => path => evenOdd => () => { host().call("canvasClip", target, path, evenOdd); };
16+export const setBlend = target => mode => () => { host().call("canvasSetBlend", target, mode); };
17+export const setBlur = target => radius => () => { host().call("canvasSetBlur", target, radius); };
18+export const setTintImpl = target => color => () => { host().call("canvasSetTint", target, color); };
19+export const fillPath = target => path => color => () => { host().call("canvasFillPath", target, path, color); };
20+export const strokePath = target => path => stroke => () => { host().call("canvasStrokePath", target, path, stroke); };
21+export const fillStrokePath = target => path => color => stroke => () => { host().call("canvasFillStrokePath", target, path, color, stroke); };
22+export const text = target => value => () => { host().call("canvasText", target, value); };
23+export const circlePattern = target => pattern => () => { host().call("canvasCirclePattern", target, pattern); };
24+export const surface = destination => source => () => { host().call("canvasSurface", destination, source); };
25+export const writePng = target => path => () => { host().call("canvasWritePng", target, path); };
src/Tauri/Native/Canvas.pursnew file+107
@@ -0,0 +1,107 @@
1+-- | Reusable display-independent CPU Skia canvases. Requires tauri-native-host.
2+-- | Surface handles must be released when their owner is finished drawing.
3+module Tauri.Native.Canvas
4+ ( Surface
5+ , Color
6+ , Affine
7+ , Rect
8+ , Path
9+ , Stroke
10+ , Text
11+ , CirclePattern
12+ , create
13+ , release
14+ , reset
15+ , clear
16+ , save
17+ , restore
18+ , setTransform
19+ , transform
20+ , clip
21+ , setBlend
22+ , setBlur
23+ , setTint
24+ , fillPath
25+ , strokePath
26+ , fillStrokePath
27+ , text
28+ , circlePattern
29+ , surface
30+ , writePng
31+ ) where
32+
33+import Prelude
34+
35+import Data.Maybe (Maybe)
36+import Data.Nullable (Nullable, toNullable)
37+import Effect (Effect)
38+
39+-- | Straight (unpremultiplied) RGBA channels in 0..255.
40+type Color = { r :: Int, g :: Int, b :: Int, a :: Int }
41+
42+type Affine = { a :: Number, b :: Number, c :: Number, d :: Number, e :: Number, f :: Number }
43+
44+type Rect = { x :: Number, y :: Number, width :: Number, height :: Number }
45+
46+-- | A range in a packed path array: 1 move(x,y), 2 line(x,y),
47+-- | 3 quad(cx,cy,x,y), 4 cubic(cx1,cy1,cx2,cy2,x,y), 5 close.
48+type Path = { values :: Array Number, offset :: Int, length :: Int }
49+
50+-- | Joins: 0 round, 1 bevel, 2 miter. Caps: 0 butt, 1 round, 2 square.
51+type Stroke = { color :: Color, width :: Number, join :: Int, cap :: Int }
52+
53+-- | CSS shorthand font, shared with Tauri.Native.measureText.
54+-- | Align: 0 left, 1 center, 2 right. Baseline: 0 top, 1 middle,
55+-- | 2 alphabetic, 3 bottom. Baselines use font metrics, not each word's ink.
56+-- | Registered OpenType features are shared by shaping and positioned drawing.
57+type Text =
58+ { x :: Number
59+ , y :: Number
60+ , content :: String
61+ , font :: String
62+ , color :: Color
63+ , align :: Int
64+ , baseline :: Int
65+ }
66+
67+type CirclePattern =
68+ { rect :: Rect
69+ , background :: Color
70+ , ink :: Color
71+ , tile :: Number
72+ , radius :: Number
73+ , originX :: Number
74+ , originY :: Number
75+ }
76+
77+-- | Apply a SrcIn color-filter tint; Nothing removes the filter.
78+setTint :: Surface -> Maybe Color -> Effect Unit
79+setTint target color = setTintImpl target (toNullable color)
80+
81+foreign import data Surface :: Type
82+
83+foreign import create :: { width :: Int, height :: Int } -> Effect Surface
84+foreign import release :: Surface -> Effect Unit
85+-- | Clear transparent and reset transform, clipping, styles and save stack.
86+foreign import reset :: Surface -> Effect Unit
87+-- | Ignore the transform while respecting the current clip.
88+foreign import clear :: Surface -> Color -> Effect Unit
89+foreign import save :: Surface -> Effect Unit
90+foreign import restore :: Surface -> Effect Unit
91+foreign import setTransform :: Surface -> Affine -> Effect Unit
92+foreign import transform :: Surface -> Affine -> Effect Unit
93+-- | True selects the even-odd fill rule.
94+foreign import clip :: Surface -> Path -> Boolean -> Effect Unit
95+-- | source-over, difference, destination-out, destination-in, source-in or copy.
96+foreign import setBlend :: Surface -> String -> Effect Unit
97+-- | Filter radius in display pixels, unaffected by the current transform.
98+foreign import setBlur :: Surface -> Number -> Effect Unit
99+foreign import setTintImpl :: Surface -> Nullable Color -> Effect Unit
100+foreign import fillPath :: Surface -> Path -> Color -> Effect Unit
101+foreign import strokePath :: Surface -> Path -> Stroke -> Effect Unit
102+foreign import fillStrokePath :: Surface -> Path -> Color -> Stroke -> Effect Unit
103+foreign import text :: Surface -> Text -> Effect Unit
104+foreign import circlePattern :: Surface -> CirclePattern -> Effect Unit
105+-- | Destination then source; draw at 0,0 using destination transform and style.
106+foreign import surface :: Surface -> Surface -> Effect Unit
107+foreign import writePng :: Surface -> String -> Effect Unit