func fixXMLId(id string) string {
	if len(id) == 0 {
		panic("No id given")
	}
	fixedId := []rune{}
	for i := 0; len(id) > 0; i++ {
		r, size := utf8.DecodeRuneInString(id)
		if i == 0 {
			if unicode.IsNumber(r) || unicode.IsPunct(r) || unicode.IsSymbol(r) {
				fixedId = append(fixedId, []rune("id")...)
			}
		}
		if !unicode.IsSpace(r) && r != ':' {
			fixedId = append(fixedId, r)
		}
		id = id[size:]
	}
	return string(fixedId)
}
fixXMLId takes a string and returns an XML id compatible string.
https://www.w3.org/TR/REC-xml-names/#NT-NCName
This means it must not contain a colon (:) or whitespace and it must not
start with a digit, punctuation or diacritics
package epub
import (
	"archive/zip"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"unicode"
	"unicode/utf8"
	"github.com/gofrs/uuid"
)
type UnableToCreateEpubError struct {
	Path string  
	Err  error   
}
func (e *UnableToCreateEpubError) Error() string {
	return fmt.Sprintf("Error creating EPUB at %q: %+v", e.Path, e.Err)
}
const (
	containerFilename     = "container.xml"
	containerFileTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="%s/%s" media-type="application/oebps-package+xml" />
  </rootfiles>
</container>
`
	contentFolderName    = "EPUB"
	coverImageProperties = "cover-image"
	dirPermissions = 0755
	filePermissions   = 0644
	mediaTypeCSS      = "text/css"
	mediaTypeEpub     = "application/epub+zip"
	mediaTypeJpeg     = "image/jpeg"
	mediaTypeNcx      = "application/x-dtbncx+xml"
	mediaTypeXhtml    = "application/xhtml+xml"
	metaInfFolderName = "META-INF"
	mimetypeFilename  = "mimetype"
	pkgFilename       = "package.opf"
	tempDirPrefix     = "go-epub"
	xhtmlFolderName   = "xhtml"
)
func (e *Epub) WriteTo(dst io.Writer) (int64, error) {
	e.Lock()
	defer e.Unlock()
	tempDir := uuid.Must(uuid.NewV4()).String()
	err := filesystem.Mkdir(tempDir, dirPermissions)
	if err != nil {
		panic(fmt.Sprintf("Error creating temp directory: %s", err))
	}
	defer func() {
		if err := filesystem.RemoveAll(tempDir); err != nil {
			panic(fmt.Sprintf("Error removing temp directory: %s", err))
		}
	}()
	writeMimetype(tempDir)
	createEpubFolders(tempDir)
	writeContainerFile(tempDir)
	err = e.writeCSSFiles(tempDir)
	if err != nil {
		return 0, err
	}
	err = e.writeFonts(tempDir)
	if err != nil {
		return 0, err
	}
	err = e.writeImages(tempDir)
	if err != nil {
		return 0, err
	}
	e.writeSections(tempDir)
	e.writeToc(tempDir)
	e.writePackageFile(tempDir)
	return e.writeEpub(tempDir, dst)
}
func (e *Epub) Write(destFilePath string) error {
	f, err := os.Create(destFilePath)
	if err != nil {
		return &UnableToCreateEpubError{
			Path: destFilePath,
			Err:  err,
		}
	}
	defer f.Close()
	_, err = e.WriteTo(f)
	return err
}
func createEpubFolders(rootEpubDir string) {
	if err := filesystem.Mkdir(
		filepath.Join(
			rootEpubDir,
			contentFolderName,
		),
		dirPermissions); err != nil {
		panic(fmt.Sprintf("Error creating EPUB subdirectory: %s", err))
	}
	if err := filesystem.Mkdir(
		filepath.Join(
			rootEpubDir,
			contentFolderName,
			xhtmlFolderName,
		),
		dirPermissions); err != nil {
		panic(fmt.Sprintf("Error creating xhtml subdirectory: %s", err))
	}
	if err := filesystem.Mkdir(
		filepath.Join(
			rootEpubDir,
			metaInfFolderName,
		),
		dirPermissions); err != nil {
		panic(fmt.Sprintf("Error creating META-INF subdirectory: %s", err))
	}
}
func writeContainerFile(rootEpubDir string) {
	containerFilePath := filepath.Join(rootEpubDir, metaInfFolderName, containerFilename)
	if err := filesystem.WriteFile(
		containerFilePath,
		[]byte(
			fmt.Sprintf(
				containerFileTemplate,
				contentFolderName,
				pkgFilename,
			),
		),
		filePermissions,
	); err != nil {
		panic(fmt.Sprintf("Error writing container file: %s", err))
	}
}
func (e *Epub) writeCSSFiles(rootEpubDir string) error {
	err := e.writeMedia(rootEpubDir, e.css, CSSFolderName)
	if err != nil {
		return err
	}
	os.Remove(e.cover.cssTempFile)
	return nil
}
type writeCounter struct {
	Total int64  
}
func (wc *writeCounter) Write(p []byte) (int, error) {
	n := len(p)
	wc.Total += int64(n)
	return n, nil
}
func (e *Epub) writeEpub(rootEpubDir string, dst io.Writer) (int64, error) {
	counter := &writeCounter{}
	teeWriter := io.MultiWriter(counter, dst)
	z := zip.NewWriter(teeWriter)
	skipMimetypeFile := false
	addFileToZip := func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		relativePath, err := filepath.Rel(rootEpubDir, path)
		if err != nil {
			return err
		}
		relativePath = filepath.ToSlash(relativePath)
		info, err := d.Info()
		if err != nil {
			return err
		}
		if !info.Mode().IsRegular() {
			return nil
		}
		var w io.Writer
		if path == filepath.Join(rootEpubDir, mimetypeFilename) {
			if skipMimetypeFile == true {
				return nil
			}
			w, err = z.CreateHeader(&zip.FileHeader{
				Name:   relativePath,
				Method: zip.Store,
			})
		} else {
			w, err = z.Create(relativePath)
		}
		if err != nil {
			return fmt.Errorf("error creating zip writer: %w", err)
		}
		r, err := filesystem.Open(path)
		if err != nil {
			return fmt.Errorf("error opening file %v being added to EPUB: %w", path, err)
		}
		defer func() {
			if err := r.Close(); err != nil {
				panic(err)
			}
		}()
		_, err = io.Copy(w, r)
		if err != nil {
			return fmt.Errorf("error copying contents of file being added EPUB: %w", err)
		}
		return nil
	}
	mimetypeFilePath := filepath.Join(rootEpubDir, mimetypeFilename)
	mimetypeInfo, err := fs.Stat(filesystem, mimetypeFilePath)
	if err != nil {
		if err := z.Close(); err != nil {
			panic(err)
		}
		return counter.Total, fmt.Errorf("unable to get FileInfo for mimetype file: %w", err)
	}
	err = addFileToZip(mimetypeFilePath, fileInfoToDirEntry(mimetypeInfo), nil)
	if err != nil {
		if err := z.Close(); err != nil {
			panic(err)
		}
		return counter.Total, fmt.Errorf("unable to add mimetype file to EPUB: %w", err)
	}
	skipMimetypeFile = true
	err = fs.WalkDir(filesystem, rootEpubDir, addFileToZip)
	if err != nil {
		if err := z.Close(); err != nil {
			panic(err)
		}
		return counter.Total, fmt.Errorf("unable to add file to EPUB: %w", err)
	}
	err = z.Close()
	return counter.Total, err
}
func (e *Epub) writeFonts(rootEpubDir string) error {
	return e.writeMedia(rootEpubDir, e.fonts, FontFolderName)
}
func (e *Epub) writeImages(rootEpubDir string) error {
	return e.writeMedia(rootEpubDir, e.images, ImageFolderName)
}
func (e *Epub) writeMedia(rootEpubDir string, mediaMap map[string]string, mediaFolderName string) error {
	if len(mediaMap) > 0 {
		mediaFolderPath := filepath.Join(rootEpubDir, contentFolderName, mediaFolderName)
		if err := filesystem.Mkdir(mediaFolderPath, dirPermissions); err != nil {
			return fmt.Errorf("unable to create directory: %s", err)
		}
		for mediaFilename, mediaSource := range mediaMap {
			mediaType, err := grabber{(e.Client)}.fetchMedia(mediaSource, mediaFolderPath, mediaFilename)
			if err != nil {
				return err
			}
			mediaProperties := ""
			if mediaFilename == e.cover.imageFilename {
				mediaProperties = coverImageProperties
			}
			e.pkg.addToManifest(fixXMLId(mediaFilename), filepath.Join(mediaFolderName, mediaFilename), mediaType, mediaProperties)
		}
	}
	return nil
}