package office import ( "bytes" "encoding/base64" "fmt" "image" "image/color" "image/png" "math" "strings" "testing" ) // tiny real PNG with a known non-4:3 aspect (100x25) func makePngDataURL(t *testing.T, w, h int) string { t.Helper() img := image.NewRGBA(image.Rect(0, 0, w, h)) for x := 0; x < w; x++ { for y := 0; y < h; y++ { img.Set(x, y, color.RGBA{R: 200, G: 30, B: 30, A: 255}) } } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { t.Fatalf("png encode: %v", err) } return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()) } func TestDocxImageKeepsNaturalAspect(t *testing.T) { // width given, no height: docx must derive height from the 100x25 // natural size (aspect 4:1), not the old 4:3 guess src := makePngDataURL(t, 100, 25) doc := &Document{HTML: `

`} data, err := BuildDocx(doc) if err != nil { t.Fatalf("BuildDocx: %v", err) } body := string(zipPart(t, data, "word/document.xml")) wantCx := pxToEmu(400) wantCy := pxToEmu(100) // 400 * 25/100 want := fmt.Sprintf(``, wantCx, wantCy) if !strings.Contains(body, want) { t.Errorf("expected %s in document.xml, got: %s", want, snippetAround(body, "wp:extent")) } } func TestDocxImageNoSizeUsesNatural(t *testing.T) { src := makePngDataURL(t, 120, 90) doc := &Document{HTML: `

`} data, err := BuildDocx(doc) if err != nil { t.Fatalf("BuildDocx: %v", err) } body := string(zipPart(t, data, "word/document.xml")) want := fmt.Sprintf(``, pxToEmu(120), pxToEmu(90)) if !strings.Contains(body, want) { t.Errorf("expected %s, got: %s", want, snippetAround(body, "wp:extent")) } } func TestDocxImageCappedToTextWidth(t *testing.T) { src := makePngDataURL(t, 100, 50) textW := textWidthPt(nil) // A4, 25.4mm margins emu := func(pt float64) int64 { return int64(math.Round(pt * 12700)) } tests := []struct { name, img string cx, cy int64 }{ // no stated height: the editor scales it with the width {"automatic height", ``, emu(textW), emu(textW / 2)}, // a stated height stays as the editor shows it (max-width only) {"stated height", ``, emu(textW), emu(200)}, {"narrow picture", ``, emu(300), emu(150)}, } for _, tc := range tests { data, err := BuildDocx(&Document{HTML: `

` + tc.img + `

`}) if err != nil { t.Fatalf("%s: BuildDocx: %v", tc.name, err) } body := string(zipPart(t, data, "word/document.xml")) want := fmt.Sprintf(``, tc.cx, tc.cy) if !strings.Contains(body, want) { t.Errorf("%s: expected %s, got: %s", tc.name, want, snippetAround(body, "wp:extent")) } } } func TestDocxTableWidthAndShading(t *testing.T) { doc := &Document{HTML: `` + `` + `` + `` + `
AB
`} data, err := BuildDocx(doc) if err != nil { t.Fatalf("BuildDocx: %v", err) } body := string(zipPart(t, data, "word/document.xml")) // full text width (A4, 25.4mm margins: 9026 twips), fixed layout if !strings.Contains(body, ``) { t.Errorf("table is not full width: %s", snippetAround(body, "tblW")) } if !strings.Contains(body, ``) { t.Error("table layout is not fixed") } // column proportions from the colgroup: 60% and 40% of 9026 twips if !strings.Contains(body, ``) || !strings.Contains(body, ``) { t.Errorf("grid columns do not follow the colgroup: %s", snippetAround(body, "tblGrid")) } if !strings.Contains(body, ``) || !strings.Contains(body, ``) { t.Errorf("cell widths not proportional: %s", snippetAround(body, "tcW")) } // theme shading + bold survive if !strings.Contains(body, ``) { t.Errorf("cell shading lost: %s", snippetAround(body, "shd")) } // ... on the cell alone: the runs inside do not shade themselves again if n := strings.Count(body, `w:fill="3C4043"`); n != 1 { t.Errorf("cell shading written %d times, want once (tcPr): %s", n, body) } } func TestDocxTableWidthRoundTrip(t *testing.T) { // a resized table: 372px of the 620px text column (60%), px colgroup doc := &Document{HTML: `` + `` + `
abc
`} data, err := BuildDocx(doc) if err != nil { t.Fatalf("BuildDocx: %v", err) } body := string(zipPart(t, data, "word/document.xml")) // 372px = 279pt = 5580 twips if !strings.Contains(body, ``) { t.Errorf("table width not 279pt: %s", snippetAround(body, "tblW")) } // px colgroup ratios (50/25/25) scaled into the grid if !strings.Contains(body, ``) || !strings.Contains(body, ``) { t.Errorf("cell widths not 50/25/25: %s", snippetAround(body, "tcW")) } back, err := ParseDocx(data) if err != nil { t.Fatalf("ParseDocx: %v", err) } if !strings.Contains(back.HTML, `class="of-table"`) { t.Errorf("imported table lost the of-table class: %s", back.HTML) } if !strings.Contains(back.HTML, "width:279pt") { t.Errorf("imported table lost its width: %s", back.HTML) } if !strings.Contains(back.HTML, ``) { t.Errorf("imported table lost column proportions: %s", back.HTML) } } func TestDocxTableShadingRoundTrip(t *testing.T) { doc := &Document{HTML: `` + `
Hx
`} data, err := BuildDocx(doc) if err != nil { t.Fatalf("BuildDocx: %v", err) } back, err := ParseDocx(data) if err != nil { t.Fatalf("ParseDocx: %v", err) } if !strings.Contains(back.HTML, "background-color:#3c4043") { t.Errorf("cell shading lost on import: %s", back.HTML) } } func snippetAround(s, needle string) string { i := strings.Index(s, needle) if i < 0 { return "(needle absent) " + s[:minInt(300, len(s))] } start := i - 80 if start < 0 { start = 0 } end := i + 220 if end > len(s) { end = len(s) } return s[start:end] } func minInt(a, b int) int { if a < b { return a } return b }