PHP Classes

File: extension/core/filterpatch.go

Recommend this page to a friend!
  Packages of Roni   Easy-Excel   extension/core/filterpatch.go   Download  
File: extension/core/filterpatch.go
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Easy-Excel
Export and read XLSX files with a PHP Go extension
Author: By
Last change:
Date: 7 days ago
Size: 5,254 bytes
 

Contents

Class file image Download
package core import ( "archive/zip" "bytes" "encoding/xml" "fmt" "io" "strings" ) // Streaming auto-filter (Phase 3). excelize's StreamWriter cannot emit // <autoFilter>, so Phase 2 degraded at save ? measured at ~10× the styled 1M // write. Instead, the saved container is patched: every entry is raw-copied // (no recompression) except the target worksheets, whose XML gets // `<autoFilter ref="?"/>` injected right after </sheetData> ? the // schema-valid position given what the StreamWriter emits (mergeCells and // friends come later in the CT_Worksheet sequence). // filterPatch maps a sheet name to its auto-filter range. type filterPatch struct { sheet string ref string } // workbook.xml / rels subsets, just enough to map sheet name ? part path. type wbXML struct { Sheets struct { Sheet []struct { Name string `xml:"name,attr"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr"` } `xml:"sheet"` } `xml:"sheets"` } type relsXML struct { Relationship []struct { ID string `xml:"Id,attr"` Target string `xml:"Target,attr"` } `xml:"Relationship"` } func sheetPartPaths(zr *zip.Reader) (map[string]string, error) { readEntry := func(name string) ([]byte, error) { for _, f := range zr.File { if f.Name == name { r, err := f.Open() if err != nil { return nil, err } defer r.Close() return io.ReadAll(r) } } return nil, fmt.Errorf("easy-excel: %s missing from container", name) } wbRaw, err := readEntry("xl/workbook.xml") if err != nil { return nil, err } relsRaw, err := readEntry("xl/_rels/workbook.xml.rels") if err != nil { return nil, err } var wb wbXML if err := xml.Unmarshal(wbRaw, &wb); err != nil { return nil, err } var rels relsXML if err := xml.Unmarshal(relsRaw, &rels); err != nil { return nil, err } byRID := make(map[string]string, len(rels.Relationship)) for _, r := range rels.Relationship { byRID[r.ID] = r.Target } out := make(map[string]string, len(wb.Sheets.Sheet)) for _, s := range wb.Sheets.Sheet { target, ok := byRID[s.RID] if !ok { continue } target = strings.TrimPrefix(target, "/xl/") target = strings.TrimPrefix(target, "./") out[s.Name] = "xl/" + strings.TrimPrefix(target, "xl/") } return out, nil } // patchAutoFilters copies the xlsx container from src to dst, injecting // auto-filter elements into the listed sheets. func patchAutoFilters(src io.ReaderAt, size int64, dst io.Writer, patches []filterPatch) error { zr, err := zip.NewReader(src, size) if err != nil { return err } parts, err := sheetPartPaths(zr) if err != nil { return err } inject := make(map[string]string, len(patches)) // part path ? ref for _, p := range patches { part, ok := parts[p.sheet] if !ok { return fmt.Errorf("easy-excel: no worksheet part for sheet %q", p.sheet) } inject[part] = p.ref } zw := zip.NewWriter(dst) for _, f := range zr.File { ref, patch := inject[f.Name] if !patch { raw, err := f.OpenRaw() if err != nil { return err } hdr := f.FileHeader w, err := zw.CreateRaw(&hdr) if err != nil { return err } if _, err := io.Copy(w, raw); err != nil { return err } continue } r, err := f.Open() if err != nil { return err } w, err := zw.Create(f.Name) if err != nil { r.Close() return err } elem := `<autoFilter ref="` + xmlEscapeAttr(ref) + `"/>` if err := injectAfterSheetData(r, w, elem); err != nil { r.Close() return fmt.Errorf("easy-excel: patch %s: %w", f.Name, err) } r.Close() } return zw.Close() } func xmlEscapeAttr(s string) string { var b bytes.Buffer _ = xml.EscapeText(&b, []byte(s)) return b.String() } // injectAfterSheetData streams src to dst inserting elem right after the // first </sheetData> (or self-closing <sheetData/>), with chunk carry-over so // the needle is found across read boundaries at constant memory. func injectAfterSheetData(src io.Reader, dst io.Writer, elem string) error { needles := []string{"</sheetData>", "<sheetData/>"} const chunkSize = 256 << 10 carryMax := len(needles[0]) - 1 buf := make([]byte, chunkSize) var carry []byte injected := false for { n, readErr := src.Read(buf) if n > 0 { window := append(carry, buf[:n]...) if !injected { for _, needle := range needles { if i := bytes.Index(window, []byte(needle)); i >= 0 { at := i + len(needle) if _, err := dst.Write(window[:at]); err != nil { return err } if _, err := io.WriteString(dst, elem); err != nil { return err } window = window[at:] injected = true break } } } keep := 0 if !injected && len(window) > carryMax { keep = carryMax } else if !injected { keep = len(window) } flushTo := len(window) - keep if _, err := dst.Write(window[:flushTo]); err != nil { return err } carry = append([]byte(nil), window[flushTo:]...) } if readErr == io.EOF { break } if readErr != nil { return readErr } } if len(carry) > 0 { if _, err := dst.Write(carry); err != nil { return err } } if !injected { return fmt.Errorf("sheetData element not found") } return nil }