refactor changes
No files matched your search
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 716 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 685 B After Width: | Height: | Size: 988 B |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1022 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,12 +1,14 @@
|
||||
<svg width="350" height="350" viewBox="0 0 350 350" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Margin Docs mark: a page with a margin rule. Glyph only, matching margin's logo-dark.svg conventions.
|
||||
App icon composition on a 512 canvas: squircle rect x=49 y=49 w=414 h=414 rx=92.5 fill #0d0c0a,
|
||||
then this glyph under transform="translate(56,48) scale(1.142857142857)".
|
||||
That scale puts every stroke centreline on a pixel centre at 32x32, and lifts the glyph 8px
|
||||
above the geometric centre for optical balance. -->
|
||||
<rect x="70" y="35" width="210" height="280" rx="14" stroke="#fcfbf7" stroke-width="10"/>
|
||||
<rect x="93" y="72" width="10" height="206" rx="5" fill="#fcfbf7"/>
|
||||
<rect x="135" y="100" width="122" height="10" rx="5" fill="#fcfbf7"/>
|
||||
<rect x="135" y="170" width="122" height="10" rx="5" fill="#fcfbf7"/>
|
||||
<rect x="135" y="240" width="80" height="10" rx="5" fill="#fcfbf7"/>
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Margin Docs mark: stacked text bars, a paragraph seen as an alignment glyph. Source of truth
|
||||
for every file in this directory; regenerate them with `pnpm tauri icon` after editing.
|
||||
Squircle 828 wide on a 1024 canvas is the macOS icon inset, and every coordinate is a multiple
|
||||
of 32 so bars and gaps land on whole pixels at 32x32. The block spans y 224..768 against a
|
||||
centre of 512, which lifts it 16 above the geometric centre for optical balance. -->
|
||||
<rect x="98" y="98" width="828" height="828" rx="185" fill="#0d0c0a"/>
|
||||
<g fill="#fcfbf7">
|
||||
<rect x="224" y="224" width="576" height="64" rx="32"/>
|
||||
<rect x="224" y="384" width="416" height="64" rx="32"/>
|
||||
<rect x="224" y="544" width="576" height="64" rx="32"/>
|
||||
<rect x="224" y="704" width="288" height="64" rx="32"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 895 B |
@@ -141,3 +141,42 @@ fn collect_installed(
|
||||
.map(|family| (*family).to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// The document's own face
|
||||
//
|
||||
// Everything above is about the two families the bundle is missing. What follows is about the one
|
||||
// the author chose: src/model/fonts.ts offers six bundled families and whatever this machine has,
|
||||
// and an export has to be handed the bytes of whichever it names.
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
/// Every family this machine has, for the "System" group of the font picker.
|
||||
///
|
||||
/// Sorted and deduplicated here rather than in the UI, because a font book has one entry per face
|
||||
/// and the picker wants one per family: a machine with the full Helvetica Neue family would
|
||||
/// otherwise show it nine times.
|
||||
#[tauri::command(async)]
|
||||
pub fn fonts_list_system() -> Vec<String> {
|
||||
let db = system_db();
|
||||
let mut names: Vec<String> = db
|
||||
.faces()
|
||||
.filter_map(|face| face.families.first().map(|(name, _)| name.clone()))
|
||||
.collect();
|
||||
names.sort();
|
||||
names.dedup();
|
||||
names
|
||||
}
|
||||
|
||||
/// The upright and italic cuts of one installed family, for a document set in a face off the
|
||||
/// machine.
|
||||
///
|
||||
/// Four queries and not two, because a family whose bold is a separate file gives fontdb a
|
||||
/// different id for it and the collection has to be loaded either way. `seen` is what stops a `.ttc`
|
||||
/// holding all four from being read four times.
|
||||
pub fn system_faces(family: &str) -> Vec<Vec<u8>> {
|
||||
let db = system_db();
|
||||
let mut fonts = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
faces_for(&db, family, &mut fonts, &mut seen);
|
||||
fonts
|
||||
}
|
||||
@@ -297,6 +297,7 @@ pub fn run() {
|
||||
pdf::pdf_write,
|
||||
grammar::grammar_available,
|
||||
grammar::grammar_check,
|
||||
fonts::fonts_list_system,
|
||||
])
|
||||
.run(context)
|
||||
.expect("error while running Margin Docs");
|
||||
|
||||
@@ -41,6 +41,46 @@ static FACES: [&[u8]; 9] = [
|
||||
include_bytes!("../fonts/HankenGrotesk-BoldItalic.ttf"),
|
||||
];
|
||||
|
||||
// The other four families src/model/fonts.ts offers, in the variable files public/fonts already
|
||||
// ships for the editor, and loaded only when a document is actually set in one.
|
||||
//
|
||||
// These carry the caveat the nine above exist to avoid, and it is not worth cutting eight more
|
||||
// static instances to fix. Typst lays a variable file out at its default instance, so a document
|
||||
// set in EB Garamond exports with its headings at the same weight as its body: the face is right,
|
||||
// the hierarchy is flatter than the screen. The two the app defaults to are the two that export
|
||||
// perfectly, and choosing one of these is choosing a face rather than a fallback.
|
||||
static EB_GARAMOND: [&[u8]; 2] = [
|
||||
include_bytes!("../../public/fonts/EBGaramond-VF.ttf"),
|
||||
include_bytes!("../../public/fonts/EBGaramond-Italic-VF.ttf"),
|
||||
];
|
||||
static LORA: [&[u8]; 2] = [
|
||||
include_bytes!("../../public/fonts/Lora-VF.ttf"),
|
||||
include_bytes!("../../public/fonts/Lora-Italic-VF.ttf"),
|
||||
];
|
||||
static SOURCE_SERIF: [&[u8]; 2] = [
|
||||
include_bytes!("../../public/fonts/SourceSerif4-VF.ttf"),
|
||||
include_bytes!("../../public/fonts/SourceSerif4-Italic-VF.ttf"),
|
||||
];
|
||||
static FRAUNCES: [&[u8]; 2] = [
|
||||
include_bytes!("../../public/fonts/Fraunces-VF.ttf"),
|
||||
include_bytes!("../../public/fonts/Fraunces-Italic-VF.ttf"),
|
||||
];
|
||||
|
||||
/// The bytes behind one `BundledFont` id from src/model/fonts.ts.
|
||||
///
|
||||
/// Literata and Hanken Grotesk are absent on purpose rather than missing: `FACES` already holds
|
||||
/// their nine static instances and they are loaded for every export, so naming either here would
|
||||
/// hand Typst the same family twice and let the variable copy win.
|
||||
fn bundled_faces(id: &str) -> &'static [&'static [u8]] {
|
||||
match id {
|
||||
"eb-garamond" => &EB_GARAMOND,
|
||||
"lora" => &LORA,
|
||||
"source-serif" => &SOURCE_SERIF,
|
||||
"fraunces" => &FRAUNCES,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
// mitex 0.2.5, vendored under src-tauri/vendor/mitex with its LICENSE, and served as ordinary
|
||||
// paths under `/mitex/` rather than through Typst's package system. A package spec would mean a
|
||||
// download on first export and a cache directory to keep, for a dependency that is 380K and never
|
||||
@@ -242,7 +282,7 @@ fn compile_once(
|
||||
Ok((bytes, warnings.into_iter().collect()))
|
||||
}
|
||||
|
||||
/// Compiles Typst source to PDF bytes, with whatever the compiler had to work around.
|
||||
/// Compiles Typst source to PDF bytes, in the app's default pair of faces.
|
||||
///
|
||||
/// Separate from the command so a test can reach it: a `#[tauri::command]` taking an `AppHandle`
|
||||
/// needs a running app, and none of the work below wants one.
|
||||
@@ -250,6 +290,22 @@ pub fn compile(
|
||||
source: String,
|
||||
images: &[ImageInput],
|
||||
root_paths: &[String],
|
||||
) -> Result<(Vec<u8>, Vec<PdfWarning>), String> {
|
||||
compile_in(source, images, root_paths, &[], &[])
|
||||
}
|
||||
|
||||
/// The same, for a document set in something other than the default.
|
||||
///
|
||||
/// `bundled` are ids from src/model/fonts.ts and `system` are family names off this machine, both
|
||||
/// as the converter named them in the preamble it wrote. Naming a face Typst has no bytes for is a
|
||||
/// warning and a fallback rather than a failure, so an id this build does not know and a family
|
||||
/// that has since been uninstalled both end in a PDF.
|
||||
pub fn compile_in(
|
||||
source: String,
|
||||
images: &[ImageInput],
|
||||
root_paths: &[String],
|
||||
bundled: &[String],
|
||||
system: &[String],
|
||||
) -> Result<(Vec<u8>, Vec<PdfWarning>), String> {
|
||||
let mut warnings: Vec<PdfWarning> = Vec::new();
|
||||
|
||||
@@ -273,6 +329,16 @@ pub fn compile(
|
||||
let mut fonts: Vec<Vec<u8>> = FACES.iter().map(|face| face.to_vec()).collect();
|
||||
fonts.append(&mut fallbacks.fonts);
|
||||
|
||||
// The face the author chose, if it is not one of the two already in `FACES`. A system family is
|
||||
// read off this machine, which is the one place an export can differ between two computers, and
|
||||
// is the caveat the picker states next to the "System" group.
|
||||
for id in bundled {
|
||||
fonts.extend(bundled_faces(id).iter().map(|face| face.to_vec()));
|
||||
}
|
||||
for family in system {
|
||||
fonts.extend(crate::fonts::system_faces(family));
|
||||
}
|
||||
|
||||
let source = format!("{}{source}", font_preamble(&fallbacks));
|
||||
|
||||
let failure = match compile_once(&source, MITEX_LIB, &binaries, &fonts) {
|
||||
@@ -322,6 +388,8 @@ pub fn pdf_compile(
|
||||
app: tauri::AppHandle,
|
||||
source: String,
|
||||
images: Vec<ImageInput>,
|
||||
bundled_fonts: Vec<String>,
|
||||
system_fonts: Vec<String>,
|
||||
) -> Result<tauri::ipc::Response, String> {
|
||||
// The same gate `fs::checked` puts in front of every other read, reached the same way it is:
|
||||
// the open roots out of shared state, then `resolve_in_roots`. The lock is dropped before the
|
||||
@@ -332,7 +400,7 @@ pub fn pdf_compile(
|
||||
open.iter().map(|root| root.path.clone()).collect()
|
||||
};
|
||||
|
||||
let (bytes, warnings) = compile(source, &images, &root_paths)?;
|
||||
let (bytes, warnings) = compile_in(source, &images, &root_paths, &bundled_fonts, &system_fonts)?;
|
||||
if !warnings.is_empty() {
|
||||
app.emit("pdf-warnings", warnings).ok();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use margin_docs_lib::dto::ImageInput;
|
||||
use margin_docs_lib::pdf::compile;
|
||||
use margin_docs_lib::pdf::{compile, compile_in};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// A real 4x4 PNG, so an image that is meant to be read is one Typst can actually decode, and one
|
||||
@@ -314,3 +314,49 @@ fn every_weight_the_converter_asks_for_has_a_face_of_its_own() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A document set in one of the four extra bundled families really is typeset in it.
|
||||
///
|
||||
/// The bytes are compiled into the binary but only loaded when the front end names the id, so the
|
||||
/// thing worth proving is that naming it changes the page: the same word in EB Garamond and in the
|
||||
/// default face has to be two different documents. A family Typst cannot see is not an error, it is
|
||||
/// a warning and a fallback, so the negative case would pass without the loading working at all.
|
||||
#[test]
|
||||
fn a_document_set_in_a_bundled_face_is_typeset_in_it() {
|
||||
let (_dir, roots) = root();
|
||||
let word = |family: &str, bundled: &[String]| {
|
||||
let source = format!("{PREAMBLE}#text(font: \"{family}\")[Margin]\n");
|
||||
compile_in(source, &[], &roots, bundled, &[]).expect("the face typesets").0
|
||||
};
|
||||
|
||||
let default = word("Literata", &[]);
|
||||
for (id, family) in [
|
||||
("eb-garamond", "EB Garamond"),
|
||||
("lora", "Lora"),
|
||||
("source-serif", "Source Serif 4"),
|
||||
("fraunces", "Fraunces"),
|
||||
] {
|
||||
let chosen = word(family, &[id.to_string()]);
|
||||
assert_ne!(default, chosen, "{family} came out as the default face");
|
||||
}
|
||||
}
|
||||
|
||||
/// A face the compiler cannot find costs a warning and never the export.
|
||||
///
|
||||
/// Both halves reach here in the ordinary run of things. A bundled id is a preference written by
|
||||
/// some other version of the app, and a system family is one the user has uninstalled since they
|
||||
/// chose it; neither is a reason to refuse somebody their PDF.
|
||||
#[test]
|
||||
fn a_face_that_is_not_there_still_exports() {
|
||||
let (_dir, roots) = root();
|
||||
let source = format!("{PREAMBLE}#text(font: \"Nothing Installed\")[Margin]\n");
|
||||
let (bytes, _) = compile_in(
|
||||
source,
|
||||
&[],
|
||||
&roots,
|
||||
&["a-face-no-build-has".to_string()],
|
||||
&["Nothing Installed".to_string()],
|
||||
)
|
||||
.expect("an unknown family falls back rather than failing");
|
||||
assert!(bytes.starts_with(b"%PDF-"), "the export still happened");
|
||||
}
|
||||