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 <project root>
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)
}
}
gitextract_vgv9217i/ ├── .gitignore ├── README.md ├── bin/ │ └── .gitignore ├── go.mod ├── go.sum ├── inverter/ │ ├── cli.go │ ├── gui.go │ ├── pdfinverter.go │ └── utils.go └── main.go
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.