Просмотр исходного кода

Merge branch 'claude/modest-bhabha-12961e' into v3.0.4

Toby Chui 1 день назад
Родитель
Сommit
d6555567a2

+ 4 - 7
src/mod/office/odf/odp_reader.go

@@ -1137,14 +1137,11 @@ func odpRunCSS(st odpStyle) string {
 	if st.Font != "" {
 	if st.Font != "" {
 		sb.WriteString("font-family:" + fonts.StackFor(st.Font, st.Font) + ";")
 		sb.WriteString("font-family:" + fonts.StackFor(st.Font, st.Font) + ";")
 	}
 	}
-	weight := 0
+	// only fo:font-weight sets a weight; a weight word in the family name
+	// belongs to that face, not to the CJK text the run falls back for
+	// (see runCSS in the pptx reader)
 	if st.Bold {
 	if st.Bold {
-		weight = 700
-	} else if w := fonts.WeightOf(st.Font); w != 0 {
-		weight = w
-	}
-	if weight != 0 {
-		sb.WriteString("font-weight:" + strconv.Itoa(weight) + ";")
+		sb.WriteString("font-weight:700;")
 	}
 	}
 	if st.Italic {
 	if st.Italic {
 		sb.WriteString("font-style:italic;")
 		sb.WriteString("font-style:italic;")

+ 29 - 0
src/mod/office/odf/odp_test.go

@@ -1,9 +1,38 @@
 package odf
 package odf
 
 
 import (
 import (
+	"strings"
 	"testing"
 	"testing"
 )
 )
 
 
+// Only fo:font-weight makes a run heavy; a weight word in the family name
+// belongs to that face, not to the CJK text the run falls back for
+func TestOdpRunCSSWeight(t *testing.T) {
+	cases := []struct {
+		name       string
+		st         odpStyle
+		wantWeight string // "" when the run must carry no font-weight
+	}{
+		{"semibold face", odpStyle{SizePt: 24, Font: "Open Sans SemiBold"}, ""},
+		{"extrabold face", odpStyle{SizePt: 24, Font: "Noto Sans ExtraBold"}, ""},
+		{"plain face", odpStyle{SizePt: 24, Font: "Arial"}, ""},
+		{"bold on a plain face", odpStyle{SizePt: 24, Font: "Arial", Bold: true}, "font-weight:700;"},
+		{"bold on a semibold face", odpStyle{SizePt: 24, Font: "Open Sans SemiBold", Bold: true}, "font-weight:700;"},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			css := odpRunCSS(c.st)
+			if c.wantWeight == "" {
+				if strings.Contains(css, "font-weight") {
+					t.Errorf("odpRunCSS = %q, want no font-weight", css)
+				}
+			} else if !strings.Contains(css, c.wantWeight) {
+				t.Errorf("odpRunCSS = %q, want %s", css, c.wantWeight)
+			}
+		})
+	}
+}
+
 // Every kind the ODF writer knows has to read back as the same kind, or a
 // Every kind the ODF writer knows has to read back as the same kind, or a
 // deck loses its shapes on a .odp round trip.
 // deck loses its shapes on a .odp round trip.
 func TestOdpShapeTypesRoundTrip(t *testing.T) {
 func TestOdpShapeTypesRoundTrip(t *testing.T) {

+ 6 - 7
src/mod/office/pptx/text.go

@@ -265,14 +265,13 @@ func runCSS(rs runStyle, scale float64) string {
 	var sb strings.Builder
 	var sb strings.Builder
 	sb.WriteString("font-size:" + units.FmtPx(units.PtToPx(rs.SizePt)*scale) + ";")
 	sb.WriteString("font-size:" + units.FmtPx(units.PtToPx(rs.SizePt)*scale) + ";")
 	sb.WriteString("font-family:" + fonts.StackFor(rs.Latin, rs.EastAsian) + ";")
 	sb.WriteString("font-family:" + fonts.StackFor(rs.Latin, rs.EastAsian) + ";")
-	weight := 0
+	// only b="1" sets a weight. A weight word in the Latin typeface ("Open
+	// Sans SemiBold") belongs to that face alone - PowerPoint draws the
+	// run's East Asian text at regular weight in the EA font - and the
+	// Latin glyphs already get the heavy face from the family name
+	// (fonts.js substitute())
 	if rs.Bold {
 	if rs.Bold {
-		weight = 700
-	} else if w := fonts.WeightOf(rs.Latin); w != 0 {
-		weight = w
-	}
-	if weight != 0 {
-		sb.WriteString("font-weight:" + strconv.Itoa(weight) + ";")
+		sb.WriteString("font-weight:700;")
 	}
 	}
 	if rs.Italic {
 	if rs.Italic {
 		sb.WriteString("font-style:italic;")
 		sb.WriteString("font-style:italic;")

+ 48 - 0
src/mod/office/pptx/textsave_test.go

@@ -68,6 +68,8 @@ func TestPptxWriterRunWeight(t *testing.T) {
 	}{
 	}{
 		// a SemiBold face at its own weight is the face, not bold on top
 		// a SemiBold face at its own weight is the face, not bold on top
 		{"semibold face", "font-family:" + stackOpenSansSemi + ";font-weight:600;", `b="0"`},
 		{"semibold face", "font-family:" + stackOpenSansSemi + ";font-weight:600;", `b="0"`},
+		// what the reader writes for a SemiBold face now: no weight at all
+		{"semibold face, no weight", "font-family:" + stackOpenSansSemi + ";", `b="0"`},
 		{"bold on a semibold face", "font-family:" + stackOpenSansSemi + ";font-weight:700;", `b="1"`},
 		{"bold on a semibold face", "font-family:" + stackOpenSansSemi + ";font-weight:700;", `b="1"`},
 		{"600 on a plain face", "font-family:Arial;font-weight:600;", `b="1"`},
 		{"600 on a plain face", "font-family:Arial;font-weight:600;", `b="1"`},
 		{"bold keyword", "font-family:Arial;font-weight:bold;", `b="1"`},
 		{"bold keyword", "font-family:Arial;font-weight:bold;", `b="1"`},
@@ -84,6 +86,52 @@ func TestPptxWriterRunWeight(t *testing.T) {
 	}
 	}
 }
 }
 
 
+// The weight a Latin typeface name implies belongs to that face, not to
+// the run: the run's East Asian text is drawn at regular weight
+func TestRunCSSWeight(t *testing.T) {
+	cases := []struct {
+		name       string
+		rs         runStyle
+		wantWeight string // "" when the run must carry no font-weight
+	}{
+		{"semibold face", runStyle{SizePt: 24, Latin: "Open Sans SemiBold", EastAsian: "MS PGothic"}, ""},
+		{"black face", runStyle{SizePt: 24, Latin: "Lato Black"}, ""},
+		{"light face", runStyle{SizePt: 24, Latin: "Open Sans Light"}, ""},
+		{"plain face", runStyle{SizePt: 24, Latin: "Arial"}, ""},
+		{"b=1 on a plain face", runStyle{SizePt: 24, Latin: "Arial", Bold: true}, "font-weight:700;"},
+		{"b=1 on a semibold face", runStyle{SizePt: 24, Latin: "Open Sans SemiBold", Bold: true}, "font-weight:700;"},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			css := runCSS(c.rs, 1)
+			if c.wantWeight == "" {
+				if strings.Contains(css, "font-weight") {
+					t.Errorf("runCSS = %q, want no font-weight", css)
+				}
+			} else if !strings.Contains(css, c.wantWeight) {
+				t.Errorf("runCSS = %q, want %s", css, c.wantWeight)
+			}
+		})
+	}
+}
+
+// A SemiBold deck saved by the suite reads back as the same face at the
+// same (unset) weight, so a round trip neither drops nor adds bold
+func TestPptxSemiBoldRoundTrip(t *testing.T) {
+	html := `<div><span style="font-size:24px;font-family:` + stackOpenSansSemi + `;">動手做一台 USB-KVM</span></div>`
+	slide, back := writeOneObject(t, textObj(html, nil))
+	if !strings.Contains(slide, `<a:latin typeface="Open Sans SemiBold"/>`) || !strings.Contains(slide, `b="0"`) {
+		t.Fatalf("saved run is not a non-bold SemiBold face:\n%s", slide)
+	}
+	got := back.Props.HTML
+	if strings.Contains(got, "font-weight") {
+		t.Errorf("read back with a weight: %s", got)
+	}
+	if !strings.Contains(got, "Open Sans SemiBold") {
+		t.Errorf("read back without the SemiBold face: %s", got)
+	}
+}
+
 func TestPptxWriterKeepsTypefaceCase(t *testing.T) {
 func TestPptxWriterKeepsTypefaceCase(t *testing.T) {
 	html := `<div><span style="font-size:24px;font-family:'Open Sans Medium','Open Sans',sans-serif;">Hi</span></div>`
 	html := `<div><span style="font-size:24px;font-family:'Open Sans Medium','Open Sans',sans-serif;">Hi</span></div>`
 	slide, _ := writeOneObject(t, textObj(html, nil))
 	slide, _ := writeOneObject(t, textObj(html, nil))

+ 5 - 3
src/web/Office/common/fonts.js

@@ -688,7 +688,7 @@ var OfficeFonts = (function () {
                     finalLoads.push(STYLE_DESC[s2].css + ' 16px "' + pl.name.replace(/["\\]/g, "") + '"');
                     finalLoads.push(STYLE_DESC[s2].css + ' 16px "' + pl.name.replace(/["\\]/g, "") + '"');
                 }
                 }
                 substituted[pl.key] = { via: pl.via || "Noto Sans", adjust: adj, kind: pl.kind,
                 substituted[pl.key] = { via: pl.via || "Noto Sans", adjust: adj, kind: pl.kind,
-                    twin: !!(pl.twin || pl.exact) };
+                    twin: !!(pl.twin || pl.exact), heavy: !!pl.heavy };
             });
             });
             probeEl.parentNode.removeChild(probeEl);
             probeEl.parentNode.removeChild(probeEl);
             if (!styleEl) {
             if (!styleEl) {
@@ -724,8 +724,10 @@ var OfficeFonts = (function () {
 
 
     /* the stand-in a family was given, for exporters that draw text
     /* the stand-in a family was given, for exporters that draw text
        themselves: { via: "Arial", adjust: [r, b, i, bi], kind: "sans",
        themselves: { via: "Arial", adjust: [r, b, i, bi], kind: "sans",
-       twin: false } or null. twin: via is a shipped metric twin, which an
-       exporter can embed in place of the original. */
+       twin: false, heavy: false } or null. twin: via is a shipped metric
+       twin, which an exporter can embed in place of the original. heavy:
+       a weighted name ("Open Sans SemiBold") drawn from via's bold file in
+       every style, whatever weight the text asks for. */
     function substituteOf(family) {
     function substituteOf(family) {
         return substituted[norm(family)] || null;
         return substituted[norm(family)] || null;
     }
     }

+ 11 - 5
src/web/Office/common/pdfcore.js

@@ -279,6 +279,10 @@ var OfficePdfCore = (function () {
                       the answer is not known until it is
                       the answer is not known until it is
          null         nothing here can show this character
          null         nothing here can show this character
 
 
+       bold: true on any of them means the face is bold whatever the run
+       asks for: a weighted family name ("Open Sans SemiBold") that the
+       screen draws from its base family's bold file (OfficeFonts.substitute).
+
        A family that is neither - a system font - is stepped over rather
        A family that is neither - a system font - is stepped over rather
        than used: its bytes are unreadable, so the character goes to the
        than used: its bytes are unreadable, so the character goes to the
        next entry, which is the shipped face for its script. */
        next entry, which is the shipped face for its script. */
@@ -306,18 +310,19 @@ var OfficePdfCore = (function () {
             var sub = OfficeFonts.substituteOf ? OfficeFonts.substituteOf(name) : null;
             var sub = OfficeFonts.substituteOf ? OfficeFonts.substituteOf(name) : null;
             // a metric twin the suite ships stood in on screen: the PDF
             // a metric twin the suite ships stood in on screen: the PDF
             // embeds that same face
             // embeds that same face
+            var sb = !!(sub && sub.heavy);
             if (sub && sub.twin) {
             if (sub && sub.twin) {
-                var tw = fonts.shipped(sub.via, bold, italic);
+                var tw = fonts.shipped(sub.via, bold || sb, italic);
                 if (!tw) {
                 if (!tw) {
-                    if (!fonts.tried(sub.via, bold, italic)) return { need: sub.via };
+                    if (!fonts.tried(sub.via, bold || sb, italic)) return { need: sub.via, bold: sb };
                 } else if (!(tw.kit && tw.kit.hasGlyphForCodePoint && !tw.kit.hasGlyphForCodePoint(cp))) {
                 } else if (!(tw.kit && tw.kit.hasGlyphForCodePoint && !tw.kit.hasGlyphForCodePoint(cp))) {
-                    return { shipped: sub.via };
+                    return { shipped: sub.via, bold: sb };
                 }
                 }
             }
             }
             if (sub && winAnsiCp(cp)) {
             if (sub && winAnsiCp(cp)) {
                 var std = STD_FAMILIES[String(sub.via).toLowerCase()] ||
                 var std = STD_FAMILIES[String(sub.via).toLowerCase()] ||
                     { serif: "TimesRoman", mono: "Courier" }[sub.kind] || "Helvetica";
                     { serif: "TimesRoman", mono: "Courier" }[sub.kind] || "Helvetica";
-                return { std: std };
+                return { std: std, bold: sb };
             }
             }
         }
         }
         return null;
         return null;
@@ -336,7 +341,7 @@ var OfficePdfCore = (function () {
             if (ignorableCp(cp)) continue;
             if (ignorableCp(cp)) continue;
             var res = resolveChar(cp, names, fonts, bold, italic);
             var res = resolveChar(cp, names, fonts, bold, italic);
             if (!res || res.need) return null;
             if (!res || res.need) return null;
-            var key = res.std ? "s:" + res.std : "f:" + res.shipped;
+            var key = (res.std ? "s:" + res.std : "f:" + res.shipped) + (res.bold ? ":b" : "");
             if (cur && cur.key === key) cur.text += ch;
             if (cur && cur.key === key) cur.text += ch;
             else { cur = { key: key, res: res, text: ch }; segs.push(cur); }
             else { cur = { key: key, res: res, text: ch }; segs.push(cur); }
         }
         }
@@ -346,6 +351,7 @@ var OfficePdfCore = (function () {
     // faceOf: the font a resolved piece is drawn in, and what of its style
     // faceOf: the font a resolved piece is drawn in, and what of its style
     // has to be synthesized (family is the name the page declared it under)
     // has to be synthesized (family is the name the page declared it under)
     function faceOf(res, fonts, bold, italic) {
     function faceOf(res, fonts, bold, italic) {
+        bold = bold || !!res.bold;
         if (res.std) return { font: fonts.std(res.std, bold, italic), synthBold: false, synthItalic: false };
         if (res.std) return { font: fonts.std(res.std, bold, italic), synthBold: false, synthItalic: false };
         var rec = fonts.shipped(res.shipped, bold, italic);
         var rec = fonts.shipped(res.shipped, bold, italic);
         if (!rec) return null;
         if (!rec) return null;

+ 5 - 3
src/web/Office/docs/docs_pdf.js

@@ -290,7 +290,8 @@ var DocsPdf = (function () {
             for (var i = 0; i < text.length; i++) {
             for (var i = 0; i < text.length; i++) {
                 var cp = text.codePointAt(i);
                 var cp = text.codePointAt(i);
                 if (cp > 0xFFFF) i++;
                 if (cp > 0xFFFF) i++;
-                fn(C.resolveChar(cp, names, fonts, bold, italic), bold, italic);
+                var res = C.resolveChar(cp, names, fonts, bold, italic);
+                fn(res, bold || !!(res && res.bold), italic);
             }
             }
         }
         }
         function eachChar(fn) {
         function eachChar(fn) {
@@ -326,10 +327,11 @@ var DocsPdf = (function () {
         var out = [];
         var out = [];
         for (var i = 0; i < segs.length; i++) {
         for (var i = 0; i < segs.length; i++) {
             var res = segs[i].res;
             var res = segs[i].res;
+            var b = bold || !!res.bold;
             if (res.std) {
             if (res.std) {
-                out.push([segs[i].text, "s:" + res.std + ":" + (bold ? 1 : 0) + ":" + (italic ? 1 : 0), 0, 0]);
+                out.push([segs[i].text, "s:" + res.std + ":" + (b ? 1 : 0) + ":" + (italic ? 1 : 0), 0, 0]);
             } else {
             } else {
-                var face = OfficeFonts.faceFor(res.shipped, bold, italic);
+                var face = OfficeFonts.faceFor(res.shipped, b, italic);
                 if (!face) return null;
                 if (!face) return null;
                 var syn = (face.synthBold || face.synthItalic) ?
                 var syn = (face.synthBold || face.synthItalic) ?
                     C.synthStyle(face.family, st.weight, italic, st.size, segs[i].text, face.synthBold, face.synthItalic) : null;
                     C.synthStyle(face.family, st.weight, italic, st.size, segs[i].text, face.synthBold, face.synthItalic) : null;

+ 2 - 1
src/web/Office/slides/slides_pdf.js

@@ -113,7 +113,8 @@ var SlidesPdf = (function () {
                 for (var i = 0; i < text.length; i++) {
                 for (var i = 0; i < text.length; i++) {
                     var cp = text.codePointAt(i);
                     var cp = text.codePointAt(i);
                     if (cp > 0xFFFF) i++;
                     if (cp > 0xFFFF) i++;
-                    fn(resolveChar(cp, names, fonts, bold, italic), bold, italic);
+                    var res = resolveChar(cp, names, fonts, bold, italic);
+                    fn(res, bold || !!(res && res.bold), italic);
                 }
                 }
             });
             });
         }
         }