Full Code of rootVIII/pdfinverter for AI

master d1a9a490f407 cached
10 files
17.6 KB
6.2k tokens
25 symbols
1 requests
Download .txt
Repository: rootVIII/pdfinverter
Branch: master
Commit: d1a9a490f407
Files: 10
Total size: 17.6 KB

Directory structure:
gitextract_vgv9217i/

├── .gitignore
├── README.md
├── bin/
│   └── .gitignore
├── go.mod
├── go.sum
├── inverter/
│   ├── cli.go
│   ├── gui.go
│   ├── pdfinverter.go
│   └── utils.go
└── main.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.pdf
qtbox
pdfinverter
icon_1024x1024.png



================================================
FILE: README.md
================================================
### PDFINVERTER - darken (or lighten) a PDF

PDFInverter (GUI and CLI) will create a new PDF at the specified
location from a source PDF. All colors will be inverted (original shown on left):

<img src="https://user-images.githubusercontent.com/30498791/166346009-2b635dda-3c79-4557-9a7b-20f5bb64f075.png" alt="example1"><br>
<img src="https://user-images.githubusercontent.com/30498791/166346010-9d05b846-c924-4012-9693-928eafbc2a83.png" alt="example2"><br>
<img src="https://user-images.githubusercontent.com/30498791/166346011-c470d255-602c-4379-a8bd-bc0e8a2085ed.png" alt="example3"><br>


Unfortunately page links are not preserved, but this program will darken PDFs making them suitable for night reading.


A 2-3 page PDF will invert very quickly. However a 400 page PDF may take 3-4 minutes.


This project should build on any platform with <a href="https://github.com/gographics/imagick">ImageMagick bindings</a> for Golang. <code>export CGO_CFLAGS_ALLOW='-Xpreprocessor'</code> may need to be executed to run/build.



The GUI is developed with Golang QT bindings:
<img src="https://user-images.githubusercontent.com/30498791/166346008-b40e110c-9fb9-4ca1-9434-0e1f5a330171.png" alt="example2">


###### Get the project and build:
<pre>
  <code>
git clone https://github.com/rootVIII/pdfinverter.git
cd &lt;project root&gt;
go build -o bin/pdfinverter
./bin/pdfinverter 
  </code>
</pre>


###### command-line usage:
<pre>
  <code>
# Required
-i     input PDF file path
-o     output PDF file path

Note:  If no command line arguments are provided, the GUI version will open.
  </code>
</pre>

<hr>
This project was developed on macOS Big Sur 11.0.1


================================================
FILE: bin/.gitignore
================================================
*
*/
!.gitignore

================================================
FILE: go.mod
================================================
module pdfinverter

go 1.18

require (
	github.com/gen2brain/go-fitz v1.19.0 // indirect
	github.com/google/uuid v1.3.0 // indirect
	github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e // indirect
	github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d // indirect
	gopkg.in/gographics/imagick.v3 v3.4.0 // indirect
)


================================================
FILE: go.sum
================================================
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gen2brain/go-fitz v1.19.0 h1:tXuT5dpsxPNn7LS8eGv2uQ04EviyGb/o2AdEr1e4W0M=
github.com/gen2brain/go-fitz v1.19.0/go.mod h1:UZAxMETTDK4UPpuh80HaRpPzgkSibUihXVzwj2ip5oQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e h1:XWcjeEtTFTOVA9Fs1w7n2XBftk5ib4oZrhzWk0B+3eA=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o=
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/gographics/imagick.v3 v3.4.0 h1:kSnbsXOWofo81VJEn/Hw8w3qqoOrfTyWwjAQzSdtPlg=
gopkg.in/gographics/imagick.v3 v3.4.0/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA=


================================================
FILE: inverter/cli.go
================================================
package inverter

// rootVIII 2020

import (
	"io/ioutil"
	"strings"
	"sync"
)

// CLI embeds/inherits App type and controls CLI application startup & processing.
type CLI struct {
	App
}

// RunApp inverts a pdf based on cmd-line arguments.
func (cli *CLI) RunApp() {
	cli.extractImage()
	files, _ := ioutil.ReadDir(cli.TmpDir)
	for _, batch := range chunk(files) {
		var wg sync.WaitGroup
		for _, fileName := range batch {
			if !strings.Contains(fileName, "out-") {
				continue
			}
			wg.Add(1)
			go cli.imageRoutine(fileName, &wg)
			cli.imgCount++
		}
		wg.Wait()
	}

	cli.writePDF()

}


================================================
FILE: inverter/gui.go
================================================
package inverter

// rootVIII 2020

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/user"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/therecipe/qt/core"
	"github.com/therecipe/qt/gui"
	"github.com/therecipe/qt/widgets"
)

// GUI embeds App Type and controls GUI application startup & processing.
type GUI struct {
	App
	window                    *widgets.QMainWindow
	inputTextBox              *widgets.QLineEdit
	outputTextBox             *widgets.QLineEdit
	statusLabel               *widgets.QLabel
	userInfo                  *user.User
	workingCount, statusCount uint8
	haveStatus, runningJob    bool
	workingTitle              string
}

// openPDFInput opens the PDF that needs to be inverted.
func (g *GUI) openPDFInput() {
	if !g.runningJob {
		g.PDFIn = widgets.QFileDialog_GetOpenFileName(
			g.window, "Open PDF", g.userInfo.HomeDir,
			"(*.pdf)", "", widgets.QFileDialog__DontUseNativeDialog)
		g.inputTextBox.SetText(g.PDFIn)
	} else {
		g.statusLabel.SetText("Job is currently running... please wait")
	}
}

// openPDFOutput sets the path for the output PDF..
func (g *GUI) openPDFOutput() {
	if !g.runningJob {
		g.PDFOut = widgets.QFileDialog_GetSaveFileName(
			g.window, "Save", g.userInfo.HomeDir, "", "",
			widgets.QFileDialog__DontUseNativeDialog)
		g.outputTextBox.SetText(g.PDFOut)
	} else {
		g.statusLabel.SetText("Job is currently running... please wait")
	}
}

// invert signals to the background go-routine that a new job is ready to be processed.
func (g *GUI) invert() {
	g.PDFIn = g.inputTextBox.Text()
	g.PDFOut = g.outputTextBox.Text()
	err := g.shouldExecute()
	if err != nil {
		g.statusLabel.SetText(err.Error())
	} else {
		g.runningJob = true
	}
}

// resetGUI sets the GUI fields and attributes back to default if a job is not running.
// Otherwise the user is warned that a job is currently being processed.
func (g *GUI) resetGUI() {
	if !g.runningJob {
		g.reset()
	} else {
		g.statusLabel.SetText("Job is currently running... please wait")
	}
}

// reset the gui and variables to inital/empty values.
func (g *GUI) reset() {
	g.PDFIn, g.PDFOut = "", ""
	g.inputTextBox.SetText(g.PDFIn)
	g.outputTextBox.SetText(g.PDFOut)
	g.imgCount, g.workingCount = 0, 0
	g.runningJob = false
	g.window.SetWindowTitle("")
}

func (g GUI) shouldExecute() error {
	if g.runningJob {
		return fmt.Errorf("Job is currently running")
	}
	if len(g.PDFIn) < 5 || strings.ToLower(g.PDFIn[len(g.PDFIn)-4:]) != ".pdf" {
		return fmt.Errorf("input file must have .pdf extension and MIME type")
	}
	if len(g.PDFOut) < 1 {
		return fmt.Errorf("invalid output file path provided")
	}
	fileStat, err := os.Stat(g.PDFIn)
	if err != nil || fileStat.IsDir() {
		return fmt.Errorf("invalid file provided: %v", err)
	}
	fileStat, err = os.Stat(g.PDFOut)
	if err == nil && fileStat.IsDir() {
		return fmt.Errorf("output path must be a .pdf file")
	}
	return nil
}

// clearStatus is a QTimer function that periodically clears any status message after 4 seconds.
func (g *GUI) clearStatus() {
	if g.statusCount > 4 {
		g.statusLabel.SetText("")
		g.haveStatus = false
		g.statusCount = 0
	}
	if len(g.statusLabel.Text()) > 0 {
		g.haveStatus = true
		g.statusCount++
	}
}

// displayStatusRunning is a QTimer() method that
// animates the title bar during processing.
func (g *GUI) displayStatusRunning() {
	if g.runningJob {
		if g.workingCount > 10 {
			g.workingCount = 0
		} else {
			g.window.SetWindowTitle(g.workingTitle[:g.workingCount])
			g.workingCount++
		}
	}
}

// removePNGs cleans up old PNGs after converting a PDF from within goroutine.
func (g GUI) removePNGs() {
	contents, _ := ioutil.ReadDir(g.TmpDir)
	for _, png := range contents {
		if strings.Contains(png.Name(), "out") {
			err := os.Remove(fmt.Sprintf("%s%s", g.TmpDir, png.Name()))
			if err != nil {
				fmt.Printf("%v\n", err)
			}
		}
	}
}

// RunApp runs the GUI version of the app. Check for new jobs and process
// found jobs in a single background goroutine to prevent the hanging GUI
// and possible spinning beach-ball for larger-sized PDFs.
func (g *GUI) RunApp() {
	go func() {
		for {
			if !g.runningJob {
				time.Sleep(time.Millisecond * 500)
			} else {
				g.extractImage()
				files, _ := ioutil.ReadDir(g.TmpDir)
				for _, batch := range chunk(files) {
					var wg sync.WaitGroup
					for _, fileName := range batch {
						if !strings.Contains(fileName, "out-") {
							continue
						}
						wg.Add(1)
						go g.imageRoutine(fileName, &wg)
						g.imgCount++
					}
					wg.Wait()
				}

				g.writePDF()
				g.statusLabel.SetText(fmt.Sprintf("%s created", filepath.Base(g.PDFOut)))
				g.reset()
				g.removePNGs()
				g.runningJob = false
			}
		}
	}()

	userInfo, err := user.Current()
	if err != nil {
		panic("Unable to extract username of current user")
	}
	g.userInfo = userInfo
	g.workingTitle = "working..."

	ui := widgets.NewQApplication(len(os.Args), os.Args)

	g.window = widgets.NewQMainWindow(nil, 0)
	g.window.SetMinimumSize2(600, 250)
	g.window.SetMaximumSize2(600, 250)
	g.window.SetWindowTitle("")

	h1 := widgets.NewQHBoxLayout()
	h2 := widgets.NewQHBoxLayout()
	h3 := widgets.NewQHBoxLayout()
	h4 := widgets.NewQHBoxLayout()
	h5 := widgets.NewQHBoxLayout()
	v := widgets.NewQVBoxLayout()

	title := widgets.NewQGraphicsScene(nil)
	title.AddText("P D F   I N V E R T E R", gui.NewQFont2("Menlo", 20, 1, false))
	view := widgets.NewQGraphicsView(nil)
	view.SetScene(title)

	timer1 := core.NewQTimer(g.window)
	timer1.ConnectTimeout(func() { g.clearStatus() })
	timer1.Start(1000)
	timer2 := core.NewQTimer(g.window)
	timer2.ConnectTimeout(func() { g.displayStatusRunning() })
	timer2.Start(500)

	inputLabel := widgets.NewQLabel(nil, 0)
	inputLabel.SetText("Input PDF:")
	g.inputTextBox = widgets.NewQLineEdit(nil)
	g.inputTextBox.SetPlaceholderText("None Selected")
	g.inputTextBox.SetFixedWidth(400)
	g.inputTextBox.SetStyleSheet("color: #00FFFF")
	inputButton := widgets.NewQPushButton2("Browse", nil)
	inputButton.ConnectClicked(func(bool) { g.openPDFInput() })
	outputLabel := widgets.NewQLabel(nil, 0)
	outputLabel.SetText("Output PDF:")
	g.outputTextBox = widgets.NewQLineEdit(nil)
	g.outputTextBox.SetPlaceholderText("None Selected")
	g.outputTextBox.SetFixedWidth(400)
	g.outputTextBox.SetStyleSheet("color: #00FFFF")
	outputButton := widgets.NewQPushButton2("Browse", nil)
	outputButton.ConnectClicked(func(bool) { g.openPDFOutput() })
	resetButton := widgets.NewQPushButton2("Reset", nil)
	resetButton.ConnectClicked(func(bool) { g.resetGUI() })
	invertButton := widgets.NewQPushButton2("Invert", nil)
	invertButton.ConnectClicked(func(bool) { g.invert() })
	g.statusLabel = widgets.NewQLabel(nil, 0)
	g.statusLabel.SetText(fmt.Sprintf("Greetings %s", g.userInfo.Username))

	h1.Layout().AddWidget(view)
	h2.Layout().AddWidget(inputLabel)
	h2.Layout().AddWidget(g.inputTextBox)
	h2.Layout().AddWidget(inputButton)
	h3.Layout().AddWidget(outputLabel)
	h3.Layout().AddWidget(g.outputTextBox)
	h3.Layout().AddWidget(outputButton)
	h4.Layout().AddWidget(resetButton)
	h4.Layout().AddWidget(invertButton)
	h5.Layout().AddWidget(g.statusLabel)

	for _, layout := range []*widgets.QHBoxLayout{h1, h2, h3, h4, h5} {
		v.AddLayout(layout, 0)
	}

	widget := widgets.NewQWidget(nil, 0)
	widget.SetLayout(v)
	g.window.SetCentralWidget(widget)
	g.window.Show()
	ui.Exec()
}


================================================
FILE: inverter/pdfinverter.go
================================================
package inverter

// rootVIII 2020

import (
	"fmt"
	"image"
	"image/color"
	"strconv"
	"sync"

	"github.com/gen2brain/go-fitz"
	"gopkg.in/gographics/imagick.v3/imagick"
)

// PDFInverter provides an interface to the CLI and GUI types.
type PDFInverter interface {
	imageRoutine(imgName string, wg *sync.WaitGroup)
	extractImage()
	iterImage(imgName string)
	writePDF()
	RunApp()
}

// App controls CLI and GUI application startup & processing.
type App struct {
	TmpDir, PDFIn, PDFOut string
	imgCount              int
}

// imageRoutine inverts the image within a goroutine.
func (app *App) imageRoutine(imgName string, wg *sync.WaitGroup) {
	defer wg.Done()
	app.iterImage(imgName)
}

// extractImage extracts PNG images from the input PDF using fitz.
func (app App) extractImage() {
	doc, err := fitz.New(app.PDFIn)
	if err != nil {
		panic(err)
	}

	defer doc.Close()

	for pageCount := 0; pageCount < doc.NumPage(); pageCount++ {
		currentImg, err := doc.Image(pageCount)
		if err != nil {
			panic(err)
		}
		writePNG(fmt.Sprintf("%sout-%06d.png", app.TmpDir, pageCount), currentImg)
	}
}

// iterImage examines each row of pixels in a PNG while creating a new image.
func (app App) iterImage(imgName string) {
	pathPNG := fmt.Sprintf("%s%s", app.TmpDir, imgName)
	currentPNG := readPNG(pathPNG)
	perimeter := currentPNG.Bounds()
	width, height := perimeter.Max.X, perimeter.Max.Y
	revised := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{width, height}})
	var currentPixel color.Color
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			red, green, blue, alpha := currentPNG.At(x, y).RGBA()
			r, g, b, a := uint8(red), uint8(green), uint8(blue), uint8(alpha)
			if r == 0x7F && g == 0x7F && b == 0x7F {
				currentPixel = color.RGBA{0x1E, 0x1B, 0x24, a}
			} else {
				currentPixel = color.RGBA{0xFF - r, 0xFF - g, 0xFF - b, a}
			}
			revised.Set(x, y, currentPixel)
		}
	}
	writePNG(pathPNG, revised)
}

// writePDF uses write images to the PDF file.
func (app *App) writePDF() {

	convertCMD := []string{"convert"}

	for index := 0; index < app.imgCount; index++ {
		var indexString string = strconv.Itoa(index)
		var leadingZeroes string
		for i := 0; i < (6 - len(indexString)); i++ {
			leadingZeroes += "0"
		}
		inputPath := fmt.Sprintf("%sout-%s%s.png", app.TmpDir, leadingZeroes, indexString)
		convertCMD = append(convertCMD, inputPath)
	}

	convertCMD = append(convertCMD, "-quality", "100", app.PDFOut)

	imagick.Initialize()
	defer imagick.Terminate()

	_, err := imagick.ConvertImageCommand(convertCMD)
	if err != nil {
		panic(err)
	}
}


================================================
FILE: inverter/utils.go
================================================
package inverter

import (
	"bytes"
	"image"
	"image/png"
	"io/ioutil"
	"os"
)

// writePNG writes an inverted PNG to disk.
func writePNG(path string, newIMG image.Image) {
	buf := &bytes.Buffer{}
	err := png.Encode(buf, newIMG)
	if err != nil {
		panic(err)
	} else {
		err = ioutil.WriteFile(path, buf.Bytes(), 0600)
		if err != nil {
			panic(err)
		}
	}
}

// readPNG reads the image to be inverted.
func readPNG(path string) image.Image {
	imgRaw, err := os.Open(path)
	defer imgRaw.Close()
	if err != nil {
		panic(err)
	}
	imgDecoded, err := png.Decode(imgRaw)
	if err != nil {
		panic(err)
	}
	return imgDecoded
}

// chunk breaks a slice of file names into evenly sized slices. The
// final slice will contain the remaining filenames.
func chunk(fileNames []os.FileInfo) [][]string {
	chunked := [][]string{}
	index, chunkSize := 0, 100

	for i := 0; i < len(fileNames)/chunkSize+1; i++ {
		section := make([]string, chunkSize)
		for j := 0; j < chunkSize && index < len(fileNames); j++ {
			section[j] = fileNames[index].Name()
			index++
		}
		chunked = append(chunked, section)
	}
	return chunked
}


================================================
FILE: main.go
================================================
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"

	"github.com/google/uuid"
	"pdfinverter/inverter"
)

// runCLI is the entry point to the cmd-line version.
func runCLI(tmpDir string) {
	inputFile := flag.String("i", "", "input file path")
	outputFile := flag.String("o", "", "output file path")
	flag.Parse()
	if len(*inputFile) < 1 || len(*outputFile) < 1 {
		fmt.Println("-i <intput PDF path> -o <output PDF path> are required")
	} else if _, err := os.Stat(*inputFile); err != nil {
		fmt.Println("invalid file path provided for -i <input>")
	} else {
		var cliInit inverter.PDFInverter
		cliInit = &inverter.CLI{
			App: inverter.App{
				TmpDir: tmpDir,
				PDFIn:  *inputFile,
				PDFOut: *outputFile,
			},
		}
		cliInit.RunApp()
	}
}

// runGUI runs the program with a QT front-end..
func runGUI(tmpDir string) {
	var guiInit inverter.PDFInverter
	guiInit = &inverter.GUI{
		App: inverter.App{
			TmpDir: tmpDir,
		},
	}
	guiInit.RunApp()
}

func main() {
	randPrefix, err := uuid.NewRandom()
	if err != nil {
		log.Fatal(err)
	}
	tmpdir, err := ioutil.TempDir("", randPrefix.String())
	if err != nil {
		log.Fatal(err)
	}

	defer os.RemoveAll(tmpdir)

	tmpdir += "/"
	if len(os.Args) > 1 {
		runCLI(tmpdir)
	} else {
		runGUI(tmpdir)
	}
}
Download .txt
gitextract_vgv9217i/

├── .gitignore
├── README.md
├── bin/
│   └── .gitignore
├── go.mod
├── go.sum
├── inverter/
│   ├── cli.go
│   ├── gui.go
│   ├── pdfinverter.go
│   └── utils.go
└── main.go
Download .txt
SYMBOL INDEX (25 symbols across 5 files)

FILE: inverter/cli.go
  type CLI (line 12) | type CLI struct
    method RunApp (line 17) | func (cli *CLI) RunApp() {

FILE: inverter/gui.go
  type GUI (line 21) | type GUI struct
    method openPDFInput (line 34) | func (g *GUI) openPDFInput() {
    method openPDFOutput (line 46) | func (g *GUI) openPDFOutput() {
    method invert (line 58) | func (g *GUI) invert() {
    method resetGUI (line 71) | func (g *GUI) resetGUI() {
    method reset (line 80) | func (g *GUI) reset() {
    method shouldExecute (line 89) | func (g GUI) shouldExecute() error {
    method clearStatus (line 111) | func (g *GUI) clearStatus() {
    method displayStatusRunning (line 125) | func (g *GUI) displayStatusRunning() {
    method removePNGs (line 137) | func (g GUI) removePNGs() {
    method RunApp (line 152) | func (g *GUI) RunApp() {

FILE: inverter/pdfinverter.go
  type PDFInverter (line 17) | type PDFInverter interface
  type App (line 26) | type App struct
    method imageRoutine (line 32) | func (app *App) imageRoutine(imgName string, wg *sync.WaitGroup) {
    method extractImage (line 38) | func (app App) extractImage() {
    method iterImage (line 56) | func (app App) iterImage(imgName string) {
    method writePDF (line 79) | func (app *App) writePDF() {

FILE: inverter/utils.go
  function writePNG (line 12) | func writePNG(path string, newIMG image.Image) {
  function readPNG (line 26) | func readPNG(path string) image.Image {
  function chunk (line 41) | func chunk(fileNames []os.FileInfo) [][]string {

FILE: main.go
  function runCLI (line 15) | func runCLI(tmpDir string) {
  function runGUI (line 37) | func runGUI(tmpDir string) {
  function main (line 47) | func main() {
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (20K chars).
[
  {
    "path": ".gitignore",
    "chars": 44,
    "preview": "*.pdf\nqtbox\npdfinverter\nicon_1024x1024.png\n\n"
  },
  {
    "path": "README.md",
    "chars": 1659,
    "preview": "### PDFINVERTER - darken (or lighten) a PDF\n\nPDFInverter (GUI and CLI) will create a new PDF at the specified\nlocation f"
  },
  {
    "path": "bin/.gitignore",
    "chars": 16,
    "preview": "*\n*/\n!.gitignore"
  },
  {
    "path": "go.mod",
    "chars": 334,
    "preview": "module pdfinverter\n\ngo 1.18\n\nrequire (\n\tgithub.com/gen2brain/go-fitz v1.19.0 // indirect\n\tgithub.com/google/uuid v1.3.0 "
  },
  {
    "path": "go.sum",
    "chars": 3048,
    "preview": "github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1"
  },
  {
    "path": "inverter/cli.go",
    "chars": 596,
    "preview": "package inverter\n\n// rootVIII 2020\n\nimport (\n\t\"io/ioutil\"\n\t\"strings\"\n\t\"sync\"\n)\n\n// CLI embeds/inherits App type and cont"
  },
  {
    "path": "inverter/gui.go",
    "chars": 7309,
    "preview": "package inverter\n\n// rootVIII 2020\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\""
  },
  {
    "path": "inverter/pdfinverter.go",
    "chars": 2591,
    "preview": "package inverter\n\n// rootVIII 2020\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/gen2brain/g"
  },
  {
    "path": "inverter/utils.go",
    "chars": 1111,
    "preview": "package inverter\n\nimport (\n\t\"bytes\"\n\t\"image\"\n\t\"image/png\"\n\t\"io/ioutil\"\n\t\"os\"\n)\n\n// writePNG writes an inverted PNG to di"
  },
  {
    "path": "main.go",
    "chars": 1264,
    "preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/google/uuid\"\n\t\"pdfinverter/inverter\"\n)\n\n//"
  }
]

About this extraction

This page contains the full source code of the rootVIII/pdfinverter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (17.6 KB), approximately 6.2k tokens, and a symbol index with 25 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!