diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6152548 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +data/ diff --git a/Variables.go b/Variables.go new file mode 100644 index 0000000..ce6d7e1 --- /dev/null +++ b/Variables.go @@ -0,0 +1,44 @@ +package ps + +import ( + "fmt" +) + +var Colors map[string]Color = map[string]Color{ + "Black": &RGB{0, 0, 0}, + "Gray": &RGB{128, 128, 128}, + "White": &RGB{255, 255, 255}, +} + +// ModeEnum determines how aggressively the package will attempt to sync with Photoshop. +type ModeEnum int + +// Holds the current mode. +var Mode ModeEnum + +// Fast mode never checks layers before returning. +const Fast ModeEnum = 2 + +// Normal Mode Always checks to see if layers are up to date +// before returning them. +const Normal ModeEnum = 0 + +// Safe Mode Always loads the document from scratch. (Very Slow) +const Safe ModeEnum = 1 + +// PSSaveOptions is an enum for options when closing a document. +type PSSaveOptions int + +func (p *PSSaveOptions) String() string { + return fmt.Sprint("", *p) +} + +// PSSaveChanges saves changes before closing documents. +const PSSaveChanges PSSaveOptions = 1 + +// PSDoNotSaveChanges closes documents without saving. +const PSDoNotSaveChanges PSSaveOptions = 2 + +// PSPromptToSaveChanges prompts the user whether to save each +// document before closing it. +const PSPromptToSaveChanges PSSaveOptions = 3 diff --git a/colors.go b/colors.go new file mode 100644 index 0000000..ec9fba9 --- /dev/null +++ b/colors.go @@ -0,0 +1,71 @@ +package ps + +import ( + "encoding/hex" + // "fmt" +) + +// Color is an interface for color objects, allowing colors to be +// used in various formats. +// +// RGB is the default format for everything. +type Color interface { + RGB() [3]int // The color in RGB format. + Hex() []uint8 // The color in hexadecimal format. +} + +// Compare determines which of two colors is "brighter". +func Compare(a, b Color) Color { + A := a.RGB() + B := b.RGB() + Aavg := (A[0] + A[1] + A[2]) / 3 + Bavg := (B[0] + B[1] + B[2]) / 3 + if Aavg > Bavg { + return a + } + return b +} + +// RGB is a color in RGB format. It fulfills the Color interface. +type RGB struct { + Red int + Green int + Blue int +} + +// RGB returns the color in RGB format. +func (r RGB) RGB() [3]int { + return [3]int{r.Red, r.Green, r.Blue} +} + +// TODO: Implement RGB.Hex() +func (r RGB) Hex() []uint8 { + return make([]uint8, 6) +} + +// Hex is a color in hexadecimal format. It fulfills the Color interface. +type Hex []uint8 + +func (h Hex) RGB() [3]int { + src := []byte(h) + dst := make([]byte, hex.DecodedLen(len(src))) + _, err := hex.Decode(dst, src) + if err != nil { + panic(err) + } + return [3]int{int(dst[0]), int(dst[1]), int(dst[2])} +} + +func (h Hex) Hex() []uint8 { + return h +} + +// Stroke represents a layer stroke effect. +type Stroke struct { + Size float32 + Color +} + +// func (s *Stroke) String() string { +// return fmt.Sprintf("%vpt %v", s.Size, s.Color.RGB()) +// } diff --git a/ps.go b/ps.go index 31c5f72..93d7090 100644 --- a/ps.go +++ b/ps.go @@ -1,70 +1,181 @@ +// Package ps is a rudimentary API between Adobe Photoshop CS5 and Golang. +// +// Most of the interaction between the two is implemented via +// Javascript and/or VBS/Applescript. +// +// Currently only works with CS5 on Windows. package ps import ( "bytes" + "errors" "fmt" + "io/ioutil" + // "log" "os" "os/exec" - "path" + "path/filepath" "runtime" "strings" ) -const ( - Cmd = "cscript.exe" - Opts = "/nologo" -) +var Cmd string +var Opts string +var pkgpath string -var PKGPATH = path.Join(os.Getenv("GOPATH"), "src", "github.com", "sbrow", "ps") +func init() { + _, file, _, _ := runtime.Caller(0) + pkgpath = filepath.Dir(file) + switch runtime.GOOS { + case "windows": + Cmd = "cscript.exe" + Opts = "/nologo" + case "darwin": + Cmd = "osacript" + } +} +// Start opens Photoshop. func Start() error { _, err := run("start") return err } -func Open(path string) ([]byte, error) { - return run("open", path) +// Close closes the active document in Photoshop. +func Close(save PSSaveOptions) error { + _, err := run("close", save.String()) + return err } -func Close() error { - _, err := run("close") +// Open opens a Photoshop document with the specified path. +// If Photoshop is not currently running, it is started before +// opening the document +func Open(path string) error { + _, err := run("open", path) return err } -func Quit() ([]byte, error) { - return run("quit") +// Quit exits Photoshop with the given saving option. +func Quit(save PSSaveOptions) error { + _, err := run("quit", save.String()) + return err } -func Js(args ...string) ([]byte, error) { - return run("dojs", args...) +// SaveAs saves the Photoshop document to the given location. +func SaveAs(path string) error { + _, err := run("save", path) + return err } + +// DoJs runs a Photoshop Javascript script file (.jsx) from the specified location. +// It can't directly return output, so instead the scripts write their output to +// a temporary file, whose contents is then read and returned. +func DoJs(path string, args ...string) (out []byte, err error) { + // Temp file for js to output to. + outpath := filepath.Join(os.Getenv("TEMP"), "js_out.txt") + // defer os.Remove(outpath) + if !strings.HasSuffix(path, ".jsx") { + path += ".jsx" + } + + args = append([]string{outpath}, args...) + + // If passed a script by name, assume it's in the default folder. + if filepath.Dir(path) == "." { + path = filepath.Join(pkgpath, "scripts", path) + } + + args = append([]string{path}, args...) + cmd, err := run("dojs", args...) + if err == nil { + file, err := ioutil.ReadFile(outpath) + if err == nil { + cmd = append(cmd, file...) + } + } + return cmd, err +} + +// Wait prints a message to the console and halts operation until the user +// signals that they are ready (by pushing enter). +// +// Useful for when you need to do something by hand in the middle of an +// otherwise automated process. func Wait(msg string) { fmt.Print(msg) var input string fmt.Scanln(&input) + fmt.Println() } +// run handles running the script files, returning output, and displaying errors. func run(name string, args ...string) ([]byte, error) { var ext string - var dir string var out bytes.Buffer - var stderr bytes.Buffer + var errs bytes.Buffer switch runtime.GOOS { case "windows": ext = ".vbs" - dir = "win" case "darwin": ext = ".applescript" - dir = "mac" } if !strings.HasSuffix(name, ext) { name += ext } - args = append([]string{Opts, path.Join(PKGPATH, dir, name)}, args...) + + if strings.Contains(name, "dojs") { + args = append([]string{Opts, filepath.Join(pkgpath, "scripts", name)}, + args[0], + fmt.Sprintf("%s", strings.Join(args[1:], ",")), + ) + } else { + args = append([]string{Opts, filepath.Join(pkgpath, "scripts", name)}, args...) + } cmd := exec.Command(Cmd, args...) cmd.Stdout = &out - cmd.Stderr = &stderr + cmd.Stderr = &errs err := cmd.Run() - return out.Bytes(), err + if err != nil || len(errs.Bytes()) != 0 { + return out.Bytes(), errors.New(string(errs.Bytes())) + } + return out.Bytes(), nil +} + +// DoAction runs the Photoshop action with name from set. +func DoAction(set, name string) error { + _, err := run("action", set, name) + return err +} + +// ApplyDataset fills out a template file with information +// from a given dataset (csv) file. It is important to note that running this +// function will change data in the Photoshop document, but will not update +// data in the Go Document struct (if any); you will have to implement syncing +// them yourself. +func ApplyDataset(name string) ([]byte, error) { + return DoJs("applyDataset.jsx", name) +} + +// JSLayer "compiles" Javascript code to get an ArtLayer with the given path. +// The output always ends with a semicolon, so if you want to access a specific +// property of the layer, you'll have to trim the output before concatenating +func JSLayer(path string, art ...bool) string { + path = strings.TrimLeft(path, "/") + pth := strings.Split(path, "/") + js := "app.activeDocument" + last := len(pth) - 1 + if len(art) > 0 { + pth = pth[:len(pth)-1] + last-- + } + if last > 0 { + for i := 0; i < last; i++ { + js += fmt.Sprintf(".layerSets.getByName('%s')", pth[i]) + } + } + if pth[last] != "" { + js += fmt.Sprintf(".artLayers.getByName('%s')", pth[last]) + } + return js + ";" } diff --git a/ps_test.go b/ps_test.go index ff497c6..04ba17d 100644 --- a/ps_test.go +++ b/ps_test.go @@ -1,59 +1,296 @@ package ps import ( + "encoding/json" "fmt" "io/ioutil" - "path" - "strings" + "os" + "path/filepath" "testing" ) -// TODO: Comparison borked -func TestRun(t *testing.T) { - out := []byte("Testing...\n") - msg, err := run("test") +func TestPkgPath(t *testing.T) { + out := filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "sbrow", "ps") + if filepath.Join(pkgpath) != out { + t.Fatal(filepath.Join(pkgpath), out) + } +} + +func TestStart(t *testing.T) { + err := Start() if err != nil { t.Fatal(err) } - if string(msg) == string(out) { - fail := fmt.Sprintf("run(test)\ngot:\t\"%s\"\nwant:\t\"%s\"\n", msg, out) - t.Fatal(fail) - } } func TestOpen(t *testing.T) { + // if testing.Short() { + // t.Skip("Skipping \"TestOpen\"") + // } + err := Open("F:\\GitLab\\dreamkeepers-psd\\Template009.1.psd") + if err != nil { + t.Fatal(err) + } +} + +func TestClose(t *testing.T) { if testing.Short() { - t.Skip("Skipping \"TestOpen\"") + t.Skip("Skipping \"TestClose\"") + } + err := Close(2) + if err != nil { + t.Fatal(err) } - Open("F:\\GitLab\\dreamkeepers-psd\\Template009.1.psd") } func TestQuit(t *testing.T) { if testing.Short() { t.Skip("Skipping \"TestQuit\"") } - Quit() + err := Quit(2) + if err != nil { + t.Fatal(err) + } +} + +func TestDoJs(t *testing.T) { + out := []byte("F:\\TEMP\\js_out.txt\r\narg\r\nargs\r\n") + script := "test.jsx" + ret, err := DoJs(script, "arg", "args") + if err != nil { + t.Fatal(err) + } + if string(ret) != string(out) { + fail := fmt.Sprintf("TestJS failed.\ngot:\t\"%s\"\nwant:\t\"%s\"", ret, out) + t.Fatal(fail) + } +} + +func TestRun(t *testing.T) { + out := []byte("hello,\r\nworld!\r\n") + msg, err := run("test", "hello,", "world!") + if err != nil { + t.Fatal(err) + } + if string(msg) != string(out) { + fail := fmt.Sprintf("TestRun faild.\ngot:\n\"%s\"\nwant:\n\"%s\"\n", msg, out) + t.Fatal(fail) + } } func TestWait(t *testing.T) { Wait("Waiting...") } -// TODO: Comparison borked -func TestJS(t *testing.T) { - out := "Testing...\n" - _, err := Js(path.Join(Folder, "test.jsx"), Folder) +func TestDoAction_Crop(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestDoAction_Crop\"") + } + err := Open("F:\\GitLab\\dreamkeepers-psd\\Template009.1.psd") + if err != nil { + t.Fatal(err) + } + err = DoAction("DK", "Crop") + if err != nil { + t.Fatal(err) + } +} + +func TestDoAction_Undo(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestDoAction_Undo\"") + } + err := DoAction("DK", "Undo") if err != nil { t.Fatal(err) } - f, err := ioutil.ReadFile(path.Join(Folder, "test.txt")) +} + +func TestSaveAs(t *testing.T) { + err := SaveAs("F:\\TEMP\\test.png") if err != nil { t.Fatal(err) } - if strings.Compare(string(f), string(out)) != 0 { - fmt.Println(f) - fmt.Println([]byte(out)) - fail := fmt.Sprintf("TestJS failed.\ngot:\t\"%s\"\nwant:\t\"%s\"", f, out) + os.Remove("F:\\TEMP\\test.png") +} + +func TestLayerSet(t *testing.T) { + _, err := NewLayerSet("Areas/TitleBackground/", nil) + if err != nil { + t.Fatal(err) + } +} + +func TestLayer(t *testing.T) { + _, err := Layer("Border/Inner Border") + if err != nil { + t.Fatal(err) + } +} + +func TestMove(t *testing.T) { + lyr, err := Layer("Group 1/Layer 1") + if err != nil { + t.Fatal(err) + } + lyr.SetPos(100, 50, "TL") +} + +func TestActiveDocument(t *testing.T) { + Mode = Safe + if testing.Short() { + t.Skip("Skipping \"TestDocument\"") + } + d, err := ActiveDocument() + if err != nil { + t.Fatal(err) + } + if d != d.artLayers[0].Parent() { + fmt.Println(d) + fmt.Println(d.artLayers[0].Parent()) + t.Fatal("ArtLayers do not have doc as parent.") + } + if d != d.layerSets[0].Parent() { + fmt.Println(d) + fmt.Println(d.layerSets[0].Parent()) + t.Fatal("LayerSets do not have doc as parent.") + } + if d.layerSets[0] != d.layerSets[0].artLayers[0].Parent() { + fmt.Println(d.layerSets[0]) + fmt.Println(d.layerSets[0].artLayers[0]) + fmt.Println(d.layerSets[0].artLayers[0].Parent()) + t.Fatal("Layerset's ArtLayers do not have correct parents") + } + // d.LayerSet("Areas").LayerSet("Bottom").ArtLayer("L Bar").SetColor(155, 255, 255) + lyr := d.LayerSet("Text").ArtLayer("speed") + if lyr == nil { + t.Fatal("lyr does not exist") + } + s := Stroke{Size: 4, Color: &RGB{0, 0, 0}} + lyr.SetStroke(s, &RGB{128, 128, 128}) + d.Dump() +} + +func TestColor(t *testing.T) { + byt, err := run("colorLayer.vbs", "255", "255", "255") + fmt.Println(string(byt)) + fmt.Println(err) + if err != nil { + + t.Fatal() + } +} + +func TestApplyDataset(t *testing.T) { + out := []byte("done!\r\n") + ret, err := ApplyDataset(" Anger") + if err != nil { + t.Fatal(err) + } + if string(ret) != string(out) { + fail := fmt.Sprintf("TestJS failed.\ngot:\t\"%s\"\nwant:\t\"%s\"", ret, out) t.Fatal(fail) } } + +func TestDocumentLayerSet(t *testing.T) { + if testing.Short() { + t.Skip("Skipping TestDocumentLayerSet") + } + d, err := ActiveDocument() + if err != nil { + t.Fatal(err) + } + set := d.LayerSet("Text") + fmt.Println(set) + for _, lyr := range set.ArtLayers() { + fmt.Println(lyr.name) + } + lyr := set.ArtLayer("id") + fmt.Println(lyr) + set = d.LayerSet("Indicators").LayerSet("Life") + fmt.Println(set) + for _, lyr := range set.ArtLayers() { + fmt.Println(lyr.name) + } +} + +func TestLoadedDoc(t *testing.T) { + var d *Document + byt, err := ioutil.ReadFile("Document.txt") + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(byt, &d) + if err != nil { + t.Fatal(err) + } + if d != d.ArtLayers()[0].Parent() { + t.Fatal("Loaded document's ArtLayers do not point to doc") + } + if d != d.LayerSets()[0].Parent() { + t.Fatal("Loaded document's LayerSets do not point to doc") + } + if d.LayerSets()[0] != d.layerSets[0].artLayers[0].Parent() { + t.Fatal("Loaded document's LayerSet's ArtLayers do not point to layerSets") + } +} + +func TestDoJs_HideLayer(t *testing.T) { + err := Open("F:\\GitLab\\dreamkeepers-psd\\Template009.1.psd") + if err != nil { + t.Fatal(err) + } + lyr, err := NewLayerSet("Areas/TitleBackground", nil) + lyr.SetVisible(false) + if err != nil { + t.Fatal(err) + } +} + +func BenchmarkDoc_Go(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := ActiveDocument() + if err != nil { + b.Fatal(err) + } + } +} + +//.8s +//.15 +func BenchmarkHideLayer(b *testing.B) { + for i := 0; i < b.N; i++ { + // _, err := Layers("Areas/TitleBackground/") + // if err != nil { + // b.Fatal(err) + // } + } +} + +// 59ns +func BenchmarkHelloWorld_go(b *testing.B) { + for i := 0; i < b.N; i++ { + fmt.Sprintf("Hello, world!") + } +} + +// ~35200000ns (.0352s) +func BenchmarkHelloWorld_vbs(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := run("helloworld") + if err != nil { + b.Fatal(err) + } + } +} + +// ~51700000 (0.0517) +func BenchmarkHelloWorld_js(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := DoJs("test.jsx", "Hello, World!") + if err != nil { + b.Fatal(err) + } + } +} diff --git a/scripts/PsIsOpen.vbs b/scripts/PsIsOpen.vbs new file mode 100644 index 0000000..2e44e2f --- /dev/null +++ b/scripts/PsIsOpen.vbs @@ -0,0 +1,12 @@ +Function IsProcessRunning( strComputer, strProcess ) + Dim Process, strObject + IsProcessRunning = False + strObject = "winmgmts://" & strComputer + For Each Process in GetObject( strObject ).InstancesOf( "win32_process" ) + If UCase( Process.name ) = UCase( strProcess ) Then + IsProcessRunning = True + Exit Function + End If + Next +End Function +wScript.Echo IsProcessRunning(".", "Photoshop.exe") diff --git a/scripts/action.vbs b/scripts/action.vbs new file mode 100644 index 0000000..231e2ce --- /dev/null +++ b/scripts/action.vbs @@ -0,0 +1,10 @@ +set appRef = CreateObject("Photoshop.Application") +' No dialogs' +dlgMode = 3 + +set desc = CreateObject( "Photoshop.ActionDescriptor" ) +set ref = CreateObject( "Photoshop.ActionReference" ) +Call ref.PutName(appRef.CharIDToTypeID("Actn"), wScript.Arguments(1)) +Call ref.PutName(appRef.CharIDToTypeID("ASet"), wScript.Arguments(0)) +Call desc.PutReference(appRef.CharIDToTypeID("null"), ref) +Call appRef.ExecuteAction(appRef.CharIDToTypeID("Ply "), desc, dlgMode) \ No newline at end of file diff --git a/scripts/activeDocName.jsx b/scripts/activeDocName.jsx new file mode 100644 index 0000000..ef8543b --- /dev/null +++ b/scripts/activeDocName.jsx @@ -0,0 +1,2 @@ +#include lib.js +var stdout = newFile(arguments[0]);stdout.writeln(app.activeDocument.name);stdout.close(); \ No newline at end of file diff --git a/scripts/applyDataset.jsx b/scripts/applyDataset.jsx new file mode 100644 index 0000000..2e35e13 --- /dev/null +++ b/scripts/applyDataset.jsx @@ -0,0 +1,15 @@ +var saveFile = File(arguments[0]); +if(saveFile.exists) + saveFile.remove(); +var idAply = charIDToTypeID("Aply"); +var desc1 = new ActionDescriptor(); +var idnull = charIDToTypeID("null"); +var ref1 = new ActionReference(); +var iddataSetClass = stringIDToTypeID("dataSetClass"); +ref1.putName(iddataSetClass, arguments[1]); +desc1.putReference(idnull, ref1); +executeAction(idAply, desc1, DialogModes.NO); +saveFile.encoding = "UTF8"; +saveFile.open("e", "TEXT", "????"); +saveFile.writeln("done!"); +saveFile.close(); \ No newline at end of file diff --git a/win/close.vbs b/scripts/close.vbs similarity index 67% rename from win/close.vbs rename to scripts/close.vbs index 1e18426..ac2275f 100644 --- a/win/close.vbs +++ b/scripts/close.vbs @@ -1,3 +1,3 @@ set App = CreateObject("Photoshop.Application") set Doc = App.activeDocument -Doc.Close \ No newline at end of file +Doc.Close(CInt(wScript.Arguments(0))) \ No newline at end of file diff --git a/scripts/colorLayer.vbs b/scripts/colorLayer.vbs new file mode 100644 index 0000000..cd5dc29 --- /dev/null +++ b/scripts/colorLayer.vbs @@ -0,0 +1,76 @@ +DIM objApp +SET objApp = CreateObject("Photoshop.Application") +DIM dialogMode +dialogMode = 3 +DIM idsetd +idsetd = objApp.CharIDToTypeID("setd") + DIM desc134 + SET desc134 = CreateObject("Photoshop.ActionDescriptor") + DIM idnull + idnull = objApp.CharIDToTypeID("null") + DIM ref44 + SET ref44 = CreateObject("Photoshop.ActionReference") + DIM idPrpr + idPrpr = objApp.CharIDToTypeID("Prpr") + DIM idLefx + idLefx = objApp.CharIDToTypeID("Lefx") + Call ref44.PutProperty(idPrpr, idLefx) + DIM idLyr + idLyr = objApp.CharIDToTypeID("Lyr ") + DIM idOrdn + idOrdn = objApp.CharIDToTypeID("Ordn") + DIM idTrgt + idTrgt = objApp.CharIDToTypeID("Trgt") + Call ref44.PutEnumerated(idLyr, idOrdn, idTrgt) + Call desc134.PutReference(idnull, ref44) + DIM idT + idT = objApp.CharIDToTypeID("T ") + DIM desc135 + SET desc135 = CreateObject("Photoshop.ActionDescriptor") + DIM idScl + idScl = objApp.CharIDToTypeID("Scl ") + DIM idPrc + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc135.PutUnitDouble(idScl, idPrc, 416.666667) + DIM idSoFi + idSoFi = objApp.CharIDToTypeID("SoFi") + DIM desc136 + SET desc136 = CreateObject("Photoshop.ActionDescriptor") + DIM idenab + idenab = objApp.CharIDToTypeID("enab") + Call desc136.PutBoolean(idenab, True) + DIM idMd + idMd = objApp.CharIDToTypeID("Md ") + DIM idBlnM + idBlnM = objApp.CharIDToTypeID("BlnM") + DIM idNrml + idNrml = objApp.CharIDToTypeID("Nrml") + Call desc136.PutEnumerated(idMd, idBlnM, idNrml) + DIM idOpct + idOpct = objApp.CharIDToTypeID("Opct") + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc136.PutUnitDouble(idOpct, idPrc, 100.000000) + DIM idClr + idClr = objApp.CharIDToTypeID("Clr ") + DIM desc137 + SET desc137 = CreateObject("Photoshop.ActionDescriptor") + DIM idRd + idRd = objApp.CharIDToTypeID("Rd ") + Call desc137.PutDouble(idRd, CInt(wScript.Arguments(0))) + ' Call desc137.PutDouble(idRd, 255) + DIM idGrn + idGrn = objApp.CharIDToTypeID("Grn ") + Call desc137.PutDouble(idGrn, Cint(wScript.Arguments(1))) + ' Call desc137.PutDouble(idGrn, 255) + DIM idBl + idBl = objApp.CharIDToTypeID("Bl ") + Call desc137.PutDouble(idBl, CInt(wScript.Arguments(2))) + ' Call desc137.PutDouble(idBl, 255) + DIM idRGBC + idRGBC = objApp.CharIDToTypeID("RGBC") + Call desc136.PutObject(idClr, idRGBC, desc137) + idSoFi = objApp.CharIDToTypeID("SoFi") + Call desc135.PutObject(idSoFi, idSoFi, desc136) + idLefx = objApp.CharIDToTypeID("Lefx") + Call desc134.PutObject(idT, idLefx, desc135) +Call objApp.ExecuteAction(idsetd, desc134, dialogMode) \ No newline at end of file diff --git a/scripts/colorStroke.vbs b/scripts/colorStroke.vbs new file mode 100644 index 0000000..b2b1f92 --- /dev/null +++ b/scripts/colorStroke.vbs @@ -0,0 +1,120 @@ +DIM objApp +SET objApp = CreateObject("Photoshop.Application") +REM Use dialog mode 3 for show no dialogs +DIM dialogMode +dialogMode = 3 +DIM idsetd +idsetd = objApp.CharIDToTypeID("setd") + DIM desc2 + SET desc2 = CreateObject("Photoshop.ActionDescriptor") + DIM idnull + idnull = objApp.CharIDToTypeID("null") + DIM ref2 + SET ref2 = CreateObject("Photoshop.ActionReference") + DIM idPrpr + idPrpr = objApp.CharIDToTypeID("Prpr") + DIM idLefx + idLefx = objApp.CharIDToTypeID("Lefx") + Call ref2.PutProperty(idPrpr, idLefx) + DIM idLyr + idLyr = objApp.CharIDToTypeID("Lyr ") + DIM idOrdn + idOrdn = objApp.CharIDToTypeID("Ordn") + DIM idTrgt + idTrgt = objApp.CharIDToTypeID("Trgt") + Call ref2.PutEnumerated(idLyr, idOrdn, idTrgt) + Call desc2.PutReference(idnull, ref2) + DIM idT + idT = objApp.CharIDToTypeID("T ") + DIM desc3 + SET desc3 = CreateObject("Photoshop.ActionDescriptor") + DIM idScl + idScl = objApp.CharIDToTypeID("Scl ") + DIM idPrc + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc3.PutUnitDouble(idScl, idPrc, 416.666667) + DIM idSoFi + idSoFi = objApp.CharIDToTypeID("SoFi") + DIM desc4 + SET desc4 = CreateObject("Photoshop.ActionDescriptor") + DIM idenab + idenab = objApp.CharIDToTypeID("enab") + Call desc4.PutBoolean(idenab, True) + DIM idMd + idMd = objApp.CharIDToTypeID("Md ") + DIM idBlnM + idBlnM = objApp.CharIDToTypeID("BlnM") + DIM idNrml + idNrml = objApp.CharIDToTypeID("Nrml") + Call desc4.PutEnumerated(idMd, idBlnM, idNrml) + DIM idOpct + idOpct = objApp.CharIDToTypeID("Opct") + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc4.PutUnitDouble(idOpct, idPrc, 100.000000) + DIM idClr + idClr = objApp.CharIDToTypeID("Clr ") + DIM desc5 + SET desc5 = CreateObject("Photoshop.ActionDescriptor") + DIM idRd + idRd = objApp.CharIDToTypeID("Rd ") + Call desc5.PutDouble(idRd, CInt(wScript.Arguments(0))) + DIM idGrn + idGrn = objApp.CharIDToTypeID("Grn ") + Call desc5.PutDouble(idGrn,CInt(wScript.Arguments(1))) + DIM idBl + idBl = objApp.CharIDToTypeID("Bl ") + Call desc5.PutDouble(idBl, CInt(wScript.Arguments(2))) + DIM idRGBC + idRGBC = objApp.CharIDToTypeID("RGBC") + Call desc4.PutObject(idClr, idRGBC, desc5) + idSoFi = objApp.CharIDToTypeID("SoFi") + Call desc3.PutObject(idSoFi, idSoFi, desc4) + DIM idFrFX + idFrFX = objApp.CharIDToTypeID("FrFX") + DIM desc6 + SET desc6 = CreateObject("Photoshop.ActionDescriptor") + idenab = objApp.CharIDToTypeID("enab") + Call desc6.PutBoolean(idenab, True) + DIM idStyl + idStyl = objApp.CharIDToTypeID("Styl") + DIM idFStl + idFStl = objApp.CharIDToTypeID("FStl") + DIM idOutF + idOutF = objApp.CharIDToTypeID("OutF") + Call desc6.PutEnumerated(idStyl, idFStl, idOutF) + DIM idPntT + idPntT = objApp.CharIDToTypeID("PntT") + DIM idFrFl + idFrFl = objApp.CharIDToTypeID("FrFl") + DIM idSClr + idSClr = objApp.CharIDToTypeID("SClr") + Call desc6.PutEnumerated(idPntT, idFrFl, idSClr) + idMd = objApp.CharIDToTypeID("Md ") + idBlnM = objApp.CharIDToTypeID("BlnM") + idNrml = objApp.CharIDToTypeID("Nrml") + Call desc6.PutEnumerated(idMd, idBlnM, idNrml) + idOpct = objApp.CharIDToTypeID("Opct") + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc6.PutUnitDouble(idOpct, idPrc, 100.000000) + DIM idSz + idSz = objApp.CharIDToTypeID("Sz ") + DIM idPxl + idPxl = objApp.CharIDToTypeID("#Pxl") + Call desc6.PutUnitDouble(idSz, idPxl, CLng(wScript.Arguments(3))) + idClr = objApp.CharIDToTypeID("Clr ") + DIM desc7 + SET desc7 = CreateObject("Photoshop.ActionDescriptor") + idRd = objApp.CharIDToTypeID("Rd ") + Call desc7.PutDouble(idRd, CInt(wScript.Arguments(4))) + idGrn = objApp.CharIDToTypeID("Grn ") + Call desc7.PutDouble(idGrn, CInt(wScript.Arguments(5))) + idBl = objApp.CharIDToTypeID("Bl ") + Call desc7.PutDouble(idBl, CInt(wScript.Arguments(6))) + idRGBC = objApp.CharIDToTypeID("RGBC") + Call desc6.PutObject(idClr, idRGBC, desc7) + idFrFX = objApp.CharIDToTypeID("FrFX") + Call desc3.PutObject(idFrFX, idFrFX, desc6) + idLefx = objApp.CharIDToTypeID("Lefx") + Call desc2.PutObject(idT, idLefx, desc3) +Call objApp.ExecuteAction(idsetd, desc2, dialogMode) + diff --git a/scripts/compilejs.jsx b/scripts/compilejs.jsx new file mode 100644 index 0000000..e732956 --- /dev/null +++ b/scripts/compilejs.jsx @@ -0,0 +1,4 @@ +#include lib.js +var stdout = newFile(arguments[0]) +eval(arguments[1]); +stdout.close() \ No newline at end of file diff --git a/scripts/dojs.vbs b/scripts/dojs.vbs new file mode 100644 index 0000000..997320b --- /dev/null +++ b/scripts/dojs.vbs @@ -0,0 +1,13 @@ + +Dim appRef +Set appRef = CreateObject("Photoshop.Application") +if wScript.Arguments.Count = 0 then + wScript.Echo "Missing parameters" +else + path = wScript.Arguments(0) + args = wScript.Arguments(1) + error = appRef.DoJavaScriptFile(path, Split(args, ",")) + if Not error = "true" and Not error = "[ActionDescriptor]" and Not error = "undefined" Then + Err.raise 1, "dojs.vbs", error + end if +end if \ No newline at end of file diff --git a/scripts/fmtText.jsx b/scripts/fmtText.jsx new file mode 100644 index 0000000..9b88947 --- /dev/null +++ b/scripts/fmtText.jsx @@ -0,0 +1,54 @@ +var start = parseInt(arguments[1]); +var end = parseInt(arguments[2]); +var fontName = arguments[3]; +var fontStyle = arguments[4]; +var colorArray = [0, 0, 0]; +if(app.activeDocument.activeLayer.kind == LayerKind.TEXT){ + var activeLayer = app.activeDocument.activeLayer; + var fontSize = activeLayer.textItem.size; + if(activeLayer.kind == LayerKind.TEXT){ + if((activeLayer.textItem.contents != "")&&(start >= 0)&&(end <= activeLayer.textItem.contents.length)){ + var idsetd = app.charIDToTypeID( "setd" ); + var action = new ActionDescriptor(); + var idnull = app.charIDToTypeID( "null" ); + var reference = new ActionReference(); + var idTxLr = app.charIDToTypeID( "TxLr" ); + var idOrdn = app.charIDToTypeID( "Ordn" ); + var idTrgt = app.charIDToTypeID( "Trgt" ); + reference.putEnumerated( idTxLr, idOrdn, idTrgt ); + action.putReference( idnull, reference ); + var idT = app.charIDToTypeID( "T " ); + var textAction = new ActionDescriptor(); + var idTxtt = app.charIDToTypeID( "Txtt" ); + var actionList = new ActionList(); + var textRange = new ActionDescriptor(); + var idFrom = app.charIDToTypeID( "From" ); + textRange.putInteger( idFrom, start ); + textRange.putInteger( idT, end ); + var idTxtS = app.charIDToTypeID( "TxtS" ); + var formatting = new ActionDescriptor(); + var idFntN = app.charIDToTypeID( "FntN" ); + formatting.putString( idFntN, fontName ); + var idFntS = app.charIDToTypeID( "FntS" ); + formatting.putString( idFntS, fontStyle ); + var idSz = app.charIDToTypeID( "Sz " ); + var idPnt = app.charIDToTypeID( "#Pnt" ); + formatting.putUnitDouble( idSz, idPnt, fontSize ); + var idClr = app.charIDToTypeID( "Clr " ); + var colorAction = new ActionDescriptor(); + var idRd = app.charIDToTypeID( "Rd " ); + colorAction.putDouble( idRd, colorArray[0] ); + var idGrn = app.charIDToTypeID( "Grn " ); + colorAction.putDouble( idGrn, colorArray[1]); + var idBl = app.charIDToTypeID( "Bl " ); + colorAction.putDouble( idBl, colorArray[2] ); + var idRGBC = app.charIDToTypeID( "RGBC" ); + formatting.putObject( idClr, idRGBC, colorAction ); + textRange.putObject( idTxtS, idTxtS, formatting ); + actionList.putObject( idTxtt, textRange ); + textAction.putList( idTxtt, actionList ); + action.putObject( idT, idTxLr, textAction ); + app.executeAction( idsetd, action, DialogModes.NO ); + } + } +} diff --git a/scripts/getActiveDoc.jsx b/scripts/getActiveDoc.jsx new file mode 100644 index 0000000..c8d2c06 --- /dev/null +++ b/scripts/getActiveDoc.jsx @@ -0,0 +1,39 @@ +#include lib.js +var stdout = newFile(arguments[0]); +var doc = app.activeDocument; +stdout.writeln(('{"Name": "'+doc.name+'", "Height":'+doc.height+ + ', "Width":'+doc.width+', "ArtLayers": [').replace(/ px/g, "")); +function layers(lyrs) { + if (typeof lyrs === 'undefined') + return; + for (var i = 0; i < lyrs.length; i++) { + var lyr = lyrs[i]; + stdout.write(('{"Name":"' + lyr.name + '", "Bounds": [[' + lyr.bounds[0] + ',' + + lyr.bounds[1] + '],[' + lyr.bounds[2] + ',' + + lyr.bounds[3] + ']], "Visible": ' + lyr.visible+', "Text":').replace(/ px/g, "")); + if (lyr.kind == LayerKind.TEXT) + stdout.write('"'+lyr.textItem.contents+'"'); + else + stdout.write("null"); + stdout.write("}") + if (i+1 != lyrs.length) + stdout.write(','); + stdout.writeln(); + } +} +layers(doc.artLayers) +stdout.writeln('], "LayerSets": ['); +function lyrSets(sets, nm) { + if (typeof sets === 'undefined') + return; + for (var i = 0; i < sets.length; i++) { + var set = sets[i]; + var name = nm + set.name + "/"; + stdout.write('{"Name": "' + set.name + '", "Visible":'+ set.visible +'}'); + if (i+1 != sets.length) + stdout.write(','); + } +} +lyrSets(doc.layerSets) +stdout.write(']}'); +stdout.close(); \ No newline at end of file diff --git a/scripts/getLayer.jsx b/scripts/getLayer.jsx new file mode 100644 index 0000000..0375de8 --- /dev/null +++ b/scripts/getLayer.jsx @@ -0,0 +1,14 @@ +#include lib.js +app.displayDialogs=DialogModes.NO +var stdout = newFile(arguments[0]); +var lyr = eval(arguments[1]); +stdout.write(('{"Name":"' + lyr.name + '","Bounds":[[' + lyr.bounds[0] + ',' + + lyr.bounds[1] + '],[' + lyr.bounds[2] + ',' + + lyr.bounds[3] + ']],"Visible":' + lyr.visible+',"Text":').replace(/ px/g, "")); +if (lyr.kind == LayerKind.TEXT) { + stdout.write('"'+lyr.textItem.contents.replace(/\r/g, "\\r")+'"'); +} +else + stdout.write(null) +stdout.writeln('}') +stdout.close(); \ No newline at end of file diff --git a/scripts/getLayerSet.jsx b/scripts/getLayerSet.jsx new file mode 100644 index 0000000..24117de --- /dev/null +++ b/scripts/getLayerSet.jsx @@ -0,0 +1,39 @@ +#include lib.js +var stdout = newFile(arguments[0]); +var set = eval(arguments[1]); +stdout.writeln('{"Name": "'+set.name+'", "Visible": '+ set.visible +', "ArtLayers":['); +stdout.flush(); +for (var i = 0; i < set.artLayers.length; i++) { + var lyr = set.artLayers[i]; + stdout.write(('{"Name":"' + lyr.name + '", "Bounds": [[' + lyr.bounds[0] + ',' + + lyr.bounds[1] + '],[' + lyr.bounds[2] + ',' + + lyr.bounds[3] + ']], "Visible": ' + lyr.visible + ',"Text":').replace(/ px/g, "")); + if (lyr.kind == LayerKind.TEXT) + stdout.write('"'+lyr.textItem.contents.replace(/\r/g, "\\r")+'"'); + else + stdout.write("null"); + stdout.write("}") + if (i != set.artLayers.length - 1) + stdout.writeln(","); + stdout.flush(); +} +stdout.writeln("]"); +stdout.write(', "LayerSets": [') +for (var i = 0; i < set.layerSets.length; i++) { + var s = set.layerSets[i]; + stdout.write('{"Name":"' + s.name + '", "Visible": ' + s.visible + '}'); + if (i < set.layerSets.length - 1) + stdout.writeln(","); + stdout.flush() +} +stdout.writeln(']') +// app.activeDocument.activeLayer=set; +// set.merge(); +// set=eval(arguments[2]); +stdout.write(', "Bounds": [[],[]]'); +// stdout.write((', "Bounds": [[' + set.bounds[0] + ',' + + // set.bounds[1] + '],[' + set.bounds[2] + ',' + + // set.bounds[3] + ']]').replace(/ px/g, "")); +stdout.write("}"); +// Undo(); +stdout.close(); \ No newline at end of file diff --git a/scripts/isVisible.jsx b/scripts/isVisible.jsx new file mode 100644 index 0000000..0e926a8 --- /dev/null +++ b/scripts/isVisible.jsx @@ -0,0 +1,2 @@ +#include lib.js +var stdout = newFile(arguments[0]);stdout.writeln(eval(arguments[1]).visible);stdout.close(); \ No newline at end of file diff --git a/scripts/layerSetBounds.jsx b/scripts/layerSetBounds.jsx new file mode 100644 index 0000000..b1def30 --- /dev/null +++ b/scripts/layerSetBounds.jsx @@ -0,0 +1,11 @@ +#include lib.js +var stdout = newFile(arguments[0]); +var set = eval(arguments[1]); +app.activeDocument.activeLayer=set; +set.merge(); +set=eval(arguments[2]); +stdout.write(('[[' + set.bounds[0] + ',' + + set.bounds[1] + '],[' + set.bounds[2] + ',' + + set.bounds[3] + ']]').replace(/ px/g, "")); +Undo(); +stdout.close(); \ No newline at end of file diff --git a/scripts/lib.js b/scripts/lib.js new file mode 100644 index 0000000..a409c0b --- /dev/null +++ b/scripts/lib.js @@ -0,0 +1,100 @@ +// Opens and returns a file, overwriting new data. +function newFile(path) { + var f = File(path) + f.encoding = "UTF8" + f.open("w") + return f +} + +File.prototype.flush = function() { + this.close() + this.open("a") +}; +function flush(file) { + file.close() + file.open("a") +} + + +// Prints an error message. +function err(e) { + return 'ERROR: ' + e.message + ' at ' + e.fileName + ':' + e.line; +} + +function bounds(lyr) { + return ('"Bounds": [[' + lyr.bounds[0] + ',' + + lyr.bounds[1] + '],[' + lyr.bounds[2] + ',' + + lyr.bounds[3] + ']]').replace(/ px/g, ""); +} + +function Undo() { + var desc = new ActionDescriptor(); + var ref = new ActionReference(); + ref.putEnumerated( charIDToTypeID( "HstS" ), charIDToTypeID( "Ordn" ), charIDToTypeID( "Prvs" )); + desc.putReference(charIDToTypeID( "null" ), ref); + executeAction( charIDToTypeID( "slct" ), desc, DialogModes.NO ); +} + +/** +* The setFormatting function sets the font, font style, point size, and RGB color of specified +* characters in a Photoshop text layer. +* +* @param start (int) the index of the insertion point *before* the character you want., +* @param end (int) the index of the insertion point following the character. +* @param fontName is a string for the font name. +* @param fontStyle is a string for the font style. +* @param fontSize (Number) the point size of the text. +* @param colorArray (Array) is the RGB color to be applied to the text. +*/ +function setFormatting(start, end, fontName, fontStyle, fontSize, colorArray) { + if(app.activeDocument.activeLayer.kind == LayerKind.TEXT){ + var activeLayer = app.activeDocument.activeLayer; + fontSize = activeLayer.textItem.size; + colorArray = [0, 0, 0]; + if(activeLayer.kind == LayerKind.TEXT){ + if((activeLayer.textItem.contents != "")&&(start >= 0)&&(end <= activeLayer.textItem.contents.length)){ + var idsetd = app.charIDToTypeID( "setd" ); + var action = new ActionDescriptor(); + var idnull = app.charIDToTypeID( "null" ); + var reference = new ActionReference(); + var idTxLr = app.charIDToTypeID( "TxLr" ); + var idOrdn = app.charIDToTypeID( "Ordn" ); + var idTrgt = app.charIDToTypeID( "Trgt" ); + reference.putEnumerated( idTxLr, idOrdn, idTrgt ); + action.putReference( idnull, reference ); + var idT = app.charIDToTypeID( "T " ); + var textAction = new ActionDescriptor(); + var idTxtt = app.charIDToTypeID( "Txtt" ); + var actionList = new ActionList(); + var textRange = new ActionDescriptor(); + var idFrom = app.charIDToTypeID( "From" ); + textRange.putInteger( idFrom, start ); + textRange.putInteger( idT, end ); + var idTxtS = app.charIDToTypeID( "TxtS" ); + var formatting = new ActionDescriptor(); + var idFntN = app.charIDToTypeID( "FntN" ); + formatting.putString( idFntN, fontName ); + var idFntS = app.charIDToTypeID( "FntS" ); + formatting.putString( idFntS, fontStyle ); + var idSz = app.charIDToTypeID( "Sz " ); + var idPnt = app.charIDToTypeID( "#Pnt" ); + formatting.putUnitDouble( idSz, idPnt, fontSize ); + var idClr = app.charIDToTypeID( "Clr " ); + var colorAction = new ActionDescriptor(); + var idRd = app.charIDToTypeID( "Rd " ); + colorAction.putDouble( idRd, colorArray[0] ); + var idGrn = app.charIDToTypeID( "Grn " ); + colorAction.putDouble( idGrn, colorArray[1]); + var idBl = app.charIDToTypeID( "Bl " ); + colorAction.putDouble( idBl, colorArray[2] ); + var idRGBC = app.charIDToTypeID( "RGBC" ); + formatting.putObject( idClr, idRGBC, colorAction ); + textRange.putObject( idTxtS, idTxtS, formatting ); + actionList.putObject( idTxtt, textRange ); + textAction.putList( idTxtt, actionList ); + action.putObject( idT, idTxLr, textAction ); + app.executeAction( idsetd, action, DialogModes.NO ); + } + } + } +} \ No newline at end of file diff --git a/scripts/moveLayer.jsx b/scripts/moveLayer.jsx new file mode 100644 index 0000000..12187eb --- /dev/null +++ b/scripts/moveLayer.jsx @@ -0,0 +1,11 @@ +#include lib.js +var stdout = newFile(arguments[0]); +var lyr = eval(arguments[1]); +lyr.translate((Number)(arguments[2]), (Number)(arguments[3])); +if (lyr.typename == 'LayerSet') { + lyr.merge() + lyr=eval(arguments[4]) + Undo(); +} +stdout.writeln('{' + bounds(lyr) + '}') +stdout.close(); \ No newline at end of file diff --git a/win/open.vbs b/scripts/open.vbs similarity index 100% rename from win/open.vbs rename to scripts/open.vbs diff --git a/win/quit.vbs b/scripts/quit.vbs similarity index 64% rename from win/quit.vbs rename to scripts/quit.vbs index 56244e6..9f77998 100644 --- a/win/quit.vbs +++ b/scripts/quit.vbs @@ -1,9 +1,8 @@ ' Close Photoshop Set appRef = CreateObject("Photoshop.Application") -wScript.echo appRef.Documents.Count Do While appRef.Documents.Count > 0 - appRef.ActiveDocument.Close(2) + appRef.ActiveDocument.Close(CInt(wScript.Arguments(0))) Loop appRef.Quit() \ No newline at end of file diff --git a/scripts/save.vbs b/scripts/save.vbs new file mode 100644 index 0000000..e23f1ac --- /dev/null +++ b/scripts/save.vbs @@ -0,0 +1,12 @@ +Set appRef = CreateObject("Photoshop.Application") +dlgMode = 3 'No dialog +set d = CreateObject( "Photoshop.ActionDescriptor" ) +Call d.PutEnumerated(appRef.CharIDToTypeID("PGIT"), appRef.CharIDToTypeID("PGIT"), appRef.CharIDToTypeID("PGIN")) +Call d.PutEnumerated(appRef.CharIDToTypeID("PNGf"), appRef.CharIDToTypeID("PNGf"), appRef.CharIDToTypeID("PGAd")) + +SET desc = CreateObject( "Photoshop.ActionDescriptor" ) +Call desc.PutObject( appRef.CharIDToTypeID("As "), appRef.CharIDToTypeID("PNGF"), d) +Call desc.PutPath( appRef.CharIDToTypeID("In "), wScript.Arguments(0)) +Call desc.PutBoolean( appRef.CharIDToTypeID("Cpy "), True ) + +Call appRef.ExecuteAction(appRef.CharIDToTypeID("save"), desc, dlgMode) \ No newline at end of file diff --git a/win/start.vbs b/scripts/start.vbs similarity index 100% rename from win/start.vbs rename to scripts/start.vbs diff --git a/scripts/test.jsx b/scripts/test.jsx new file mode 100644 index 0000000..7f6b4e1 --- /dev/null +++ b/scripts/test.jsx @@ -0,0 +1,13 @@ +#include lib.js + +// var saveFile = File(arguments[0]); +var arg = 'app.activeDocument.layerSets.getByName("ResolveGem");'; +var set = eval(arg); +set.visible=false; +alert(set.visible) +// var doc=app.activeDocument +// doc.layerSets.getByName("ResolveGem").merge(); +// alert(doc.artLayers.getByName("ResolveGem").bounds); +// doc.activeHistoryState=doc.historyStates[doc.historyStates.length-2] +// setFormatting(0,6, "Arial", "Bold"); +// saveFile.close(); \ No newline at end of file diff --git a/scripts/test.vbs b/scripts/test.vbs new file mode 100644 index 0000000..b413227 --- /dev/null +++ b/scripts/test.vbs @@ -0,0 +1,3 @@ +for i=0 to wScript.Arguments.length-1 + wScript.echo wScript.Arguments(i) +next \ No newline at end of file diff --git a/structs.go b/structs.go new file mode 100644 index 0000000..f29edfb --- /dev/null +++ b/structs.go @@ -0,0 +1,721 @@ +// TODO: Count skipped steps. +package ps + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "strings" +) + +// Group represents a Document or LayerSet. +type Group interface { + Name() string + Parent() Group + SetParent(Group) + Path() string + ArtLayers() []*ArtLayer + LayerSets() []*LayerSet + MarshalJSON() ([]byte, error) + UnmarshalJSON(b []byte) error +} + +// Document represents a Photoshop document (PSD file). +type Document struct { + name string + height int + width int + artLayers []*ArtLayer + layerSets []*LayerSet +} + +type DocumentJSON struct { + Name string + Height int + Width int + ArtLayers []*ArtLayer + LayerSets []*LayerSet +} + +func (d *Document) MarshalJSON() ([]byte, error) { + return json.Marshal(&DocumentJSON{Name: d.name, Height: d.height, + Width: d.width, ArtLayers: d.artLayers, LayerSets: d.layerSets}) +} + +func (d *Document) UnmarshalJSON(b []byte) error { + tmp := &DocumentJSON{} + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + d.name = tmp.Name + d.height = tmp.Height + d.width = tmp.Width + d.artLayers = tmp.ArtLayers + for _, lyr := range d.artLayers { + lyr.SetParent(d) + } + d.layerSets = tmp.LayerSets + for _, set := range d.layerSets { + set.SetParent(d) + } + return nil +} + +// Name returns the document's title. +// This fufills the Group interface. +func (d *Document) Name() string { + return d.name +} + +// The height of the document, in pixels. +func (d *Document) Height() int { + return d.height +} + +func (d *Document) ArtLayers() []*ArtLayer { + return d.artLayers +} + +// LayerSets returns all the document's top level LayerSets. +func (d *Document) LayerSets() []*LayerSet { + return d.layerSets +} + +// LayerSet returns the first top level LayerSet matching +// the given name. +func (d *Document) LayerSet(name string) *LayerSet { + for _, set := range d.layerSets { + if set.name == name { + if Mode != Fast && !set.current { + set.Refresh() + } + return set + } + } + return nil +} + +func (d *Document) Parent() Group { + return nil +} +func (d *Document) SetParent(g Group) {} + +func (d *Document) Path() string { + return "" +} + +// Filename returns the path to the json file for this document. +func (d *Document) Filename() string { + _, dir, _, ok := runtime.Caller(0) + if !ok { + log.Panic("No caller information") + } + return filepath.Join(filepath.Dir(dir), "data", + strings.TrimRight(string(d.name), "\r\n")+".txt") +} + +func ActiveDocument() (*Document, error) { + log.Println("Loading ActiveDoucment") + d := &Document{} + + byt, err := DoJs("activeDocName.jsx") + if err != nil { + return nil, err + } + d.name = strings.TrimRight(string(byt), "\r\n") + if Mode != Safe { + byt, err = ioutil.ReadFile(d.Filename()) + if err == nil { + log.Println("Previous version found, loading") + err = json.Unmarshal(byt, &d) + if err == nil { + return d, err + } + } + } + log.Println("Loading manually (This could take awhile)") + byt, err = DoJs("getActiveDoc.jsx") + if err != nil { + log.Panic(err) + } + err = json.Unmarshal(byt, &d) + if err != nil { + d.Dump() + fmt.Println(string(byt)) + log.Panic(err) + } + for _, lyr := range d.artLayers { + lyr.SetParent(d) + } + for i, set := range d.layerSets { + s, err := NewLayerSet(set.Path()+"/", d) + if err != nil { + log.Fatal(err) + } + d.layerSets[i] = s + s.SetParent(d) + } + d.Dump() + return d, err +} + +func (d *Document) Dump() { + log.Println("Dumping to disk") + f, err := os.Create(d.Filename()) + if err != nil { + log.Fatal(err) + } + defer f.Close() + byt, err := json.MarshalIndent(d, "", "\t") + if err != nil { + log.Fatal(err) + } + f.Write(byt) +} + +// ArtLayer reflects certain values from an Art Layer in a Photoshop document. +// TODO: Make TextLayer a subclass of ArtLayer +type ArtLayer struct { + name string // The layer's name. + Text *string // The contents of a text layer. + bounds [2][2]int // The layers' corners. + parent Group // The LayerSet/Document this layer is in. + visible bool // Whether or not the layer is visible. + current bool // Whether we've checked this layer since we loaded from disk. + Color // The layer's color overlay. + *Stroke // The layer's stroke. +} + +// Bounds returns the furthest corners of the ArtLayer. +func (a *ArtLayer) Bounds() [2][2]int { + return a.bounds +} + +// ArtLayerJSON is a bridge between the ArtLayer struct and +// the encoding/json package, allowing ArtLayer's unexported fields +// to ber written to and read from by the json package. +type ArtLayerJSON struct { + Name string + Bounds [2][2]int + Visible bool + Color [3]int + Stroke [3]int + StrokeAmt float32 + Text *string +} + +func (a *ArtLayer) SetText(txt string) { + lyr := strings.TrimRight(JSLayer(a.Path()), ";") + js := fmt.Sprintf("%s.textItem.contents='%s';", lyr, txt) + _, err := DoJs("compilejs.jsx", js) + if err != nil { + a.Text = &txt + } + a.Refresh() +} + +// MarshalJSON fulfills the json.Marshaler interface, allowing the ArtLayer to be +// saved to disk in JSON format. +func (a *ArtLayer) MarshalJSON() ([]byte, error) { + // txt := strings.Replace(*a.Text, "\r", "\\r", -1) + return json.Marshal(&ArtLayerJSON{ + Name: a.name, + Bounds: a.bounds, + Visible: a.visible, + Color: a.Color.RGB(), + Stroke: a.Stroke.RGB(), + StrokeAmt: a.Stroke.Size, + Text: a.Text, + }) +} + +func (a *ArtLayer) UnmarshalJSON(b []byte) error { + tmp := &ArtLayerJSON{} + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + a.name = tmp.Name + a.bounds = tmp.Bounds + a.Color = RGB{tmp.Color[0], tmp.Color[1], tmp.Color[2]} + a.Stroke = &Stroke{tmp.StrokeAmt, RGB{tmp.Stroke[0], tmp.Stroke[1], tmp.Stroke[2]}} + a.visible = tmp.Visible + if tmp.Text != nil { + // s := strings.Replace(*tmp.Text, "\\r", "\r", -1) + a.Text = tmp.Text + } + a.current = false + return nil +} + +func (a *ArtLayer) Name() string { + return a.name +} + +// X1 returns the layer's leftmost x value. +func (a *ArtLayer) X1() int { + return a.bounds[0][0] +} + +// X2 returns the layer's rightmost x value. +func (a *ArtLayer) X2() int { + return a.bounds[1][0] +} + +// Y1 returns the layer's topmost y value. +func (a *ArtLayer) Y1() int { + return a.bounds[0][1] +} + +// Y2 returns the layer's bottommost y value. +func (a *ArtLayer) Y2() int { + return a.bounds[1][1] +} + +func (a *ArtLayer) SetParent(c Group) { + a.parent = c +} + +// SetActive makes this layer active in Photoshop. +// Layers need to be active to perform certain operations +func (a *ArtLayer) SetActive() ([]byte, error) { + js := fmt.Sprintf("app.activeDocument.activeLayer=%s", JSLayer(a.Path())) + return DoJs("compilejs.jsx", js) +} + +// SetColor creates a color overlay for the layer +func (a *ArtLayer) SetColor(c Color) { + if a.Color.RGB() == c.RGB() { + if Mode == 2 || (Mode == 0 && a.current) { + // log.Println("Skipping color: already set.") + return + } + } + if a.Stroke.Size != 0 { + a.SetStroke(*a.Stroke, c) + return + } + a.Color = c + cols := a.Color.RGB() + log.Printf(`Setting layer "%s" to color %v`, a.name, cols) + r := cols[0] + g := cols[1] + b := cols[2] + byt, err := a.SetActive() + if len(byt) != 0 { + log.Println(string(byt), "err") + } + if err != nil { + log.Println(a.Path()) + log.Panic(err) + } + byt, err = run("colorLayer", fmt.Sprint(r), fmt.Sprint(g), fmt.Sprint(b)) + if len(byt) != 0 { + log.Println(string(byt), "err") + } + if err != nil { + log.Panic(err) + } +} + +func (a *ArtLayer) SetStroke(stk Stroke, fill Color) { + if stk.Size == 0 { + a.Stroke = &stk + a.SetColor(fill) + return + } + if fill == nil { + fill = a.Color + } + if stk.Size == a.Stroke.Size && stk.Color.RGB() == a.Stroke.Color.RGB() { + if a.Color.RGB() == fill.RGB() { + if Mode == 2 || (Mode == 0 && a.current) { + // log.Println("Skipping stroke: already set.") + return + } + } + } + byt, err := a.SetActive() + if len(byt) != 0 { + log.Println(string(byt)) + } + if err != nil { + log.Panic(err) + } + a.Stroke = &stk + a.Color = fill + stkCol := stk.Color.RGB() + col := fill.RGB() + log.Printf("Setting layer %s stroke to %.2fpt %v and color to %v\n", a.name, a.Stroke.Size, + a.Stroke.Color.RGB(), a.Color.RGB()) + byt, err = run("colorStroke", fmt.Sprint(col[0]), fmt.Sprint(col[1]), fmt.Sprint(col[2]), + fmt.Sprintf("%.2f", stk.Size), fmt.Sprint(stkCol[0]), fmt.Sprint(stkCol[1]), fmt.Sprint(stkCol[2])) + if len(byt) != 0 { + log.Println(string(byt)) + } + if err != nil { + log.Panic(err) + } +} + +func (a *ArtLayer) Parent() Group { + return a.parent +} + +func (a *ArtLayer) Path() string { + return fmt.Sprintf("%s%s", a.parent.Path(), a.name) +} + +func (a *ArtLayer) Format(start, end int, font, style string) { + if !a.Visible() { + return + } + _, err := DoJs("fmtText.jsx", fmt.Sprint(start), fmt.Sprint(end), + font, style) + if err != nil { + log.Panic(err) + } +} + +// Layer returns an ArtLayer from the active document given a specified +// path string. +func Layer(path string) (ArtLayer, error) { + byt, err := DoJs("getLayer.jsx", JSLayer(path)) + var out ArtLayer + err = json.Unmarshal(byt, &out) + if err != nil { + return ArtLayer{}, err + } + return out, err +} + +// SetVisible makes the layer visible. +func (a *ArtLayer) SetVisible(b bool) { + if a.visible == b { + return + } + a.visible = b + switch b { + case true: + log.Printf("Showing %s", a.name) + case false: + log.Printf("Hiding %s", a.name) + } + js := fmt.Sprintf("%s.visible=%v;", + strings.TrimRight(JSLayer(a.Path()), ";"), b) + DoJs("compilejs.jsx", js) +} + +// Visible returns whether or not the layer is currently hidden. +func (a *ArtLayer) Visible() bool { + return a.visible +} + +// SetPos snaps the given layer boundry to the given point. +// Valid options for bound are: "TL", "TR", "BL", "BR" +// +// TODO: Test TR and BR +func (a *ArtLayer) SetPos(x, y int, bound string) { + if !a.visible || (x == 0 && y == 0) { + return + } + var lyrX, lyrY int + switch bound[:1] { + case "B": + lyrY = a.Y2() + case "T": + fallthrough + default: + lyrY = a.Y1() + } + switch bound[1:] { + case "R": + lyrX = a.X2() + case "L": + fallthrough + default: + lyrX = a.X1() + } + byt, err := DoJs("moveLayer.jsx", JSLayer(a.Path()), fmt.Sprint(x-lyrX), fmt.Sprint(y-lyrY)) + if err != nil { + panic(err) + } + var lyr ArtLayer + err = json.Unmarshal(byt, &lyr) + if err != nil { + log.Panic(err) + } + a.bounds = lyr.bounds +} + +func (a *ArtLayer) Refresh() error { + tmp, err := Layer(a.Path()) + if err != nil { + return err + } + tmp.SetParent(a.Parent()) + a.name = tmp.name + a.bounds = tmp.bounds + a.Text = tmp.Text + a.parent = tmp.Parent() + a.visible = tmp.visible + a.current = true + return nil +} + +type LayerSet struct { + name string + bounds [2][2]int + parent Group + current bool // Whether we've checked this layer since we loaded from disk. + visible bool + artLayers []*ArtLayer + layerSets []*LayerSet +} + +type LayerSetJSON struct { + Name string + Bounds [2][2]int + Visible bool + ArtLayers []*ArtLayer + LayerSets []*LayerSet +} + +func (l *LayerSet) MarshalJSON() ([]byte, error) { + return json.Marshal(&LayerSetJSON{ + Name: l.name, + Bounds: l.bounds, + Visible: l.visible, + ArtLayers: l.artLayers, + LayerSets: l.layerSets, + }) +} + +func (l *LayerSet) UnmarshalJSON(b []byte) error { + tmp := &LayerSetJSON{} + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + l.name = tmp.Name + l.bounds = tmp.Bounds + l.visible = tmp.Visible + l.artLayers = tmp.ArtLayers + for _, lyr := range l.artLayers { + lyr.SetParent(l) + } + l.layerSets = tmp.LayerSets + for _, set := range l.layerSets { + set.SetParent(l) + } + l.current = false + return nil +} + +func (l *LayerSet) Name() string { + return l.name +} + +func (l *LayerSet) ArtLayers() []*ArtLayer { + if Mode != 2 { + for _, lyr := range l.artLayers { + if !lyr.current { + lyr.Refresh() + } + } + } + return l.artLayers +} + +// ArtLayer returns the first top level ArtLayer matching +// the given name. +// TODO: Does funky things when passed invalid layername. +func (l *LayerSet) ArtLayer(name string) *ArtLayer { + for _, lyr := range l.artLayers { + if lyr.name == name { + if Mode == 0 && !lyr.current { + err := lyr.Refresh() + if err != nil { + l.Refresh() + err := lyr.Refresh() + if err != nil { + log.Panic(err) + } + } + } + return lyr + } + } + // l.Refresh() + // for _, lyr := range l.artLayers { + // fmt.Println(lyr) + // } + lyr := l.ArtLayer(name) + fmt.Println(lyr) + if lyr == nil { + log.Panic(errors.New("Layer not found!")) + } + return lyr +} + +func (l *LayerSet) LayerSets() []*LayerSet { + return l.layerSets +} + +// LayerSet returns the first top level LayerSet matching +// the given name. +func (l *LayerSet) LayerSet(name string) *LayerSet { + for _, set := range l.layerSets { + if set.name == name { + return set + } + } + return nil +} + +// Bounds returns the furthest corners of the LayerSet. +func (l *LayerSet) Bounds() [2][2]int { + return l.bounds +} + +func (l *LayerSet) SetParent(c Group) { + l.parent = c +} + +func (l *LayerSet) Parent() Group { + return l.parent +} + +func (l *LayerSet) Path() string { + if l.parent == nil { + return l.name + } + return fmt.Sprintf("%s%s/", l.parent.Path(), l.name) +} + +func NewLayerSet(path string, g Group) (*LayerSet, error) { + path = strings.Replace(path, "//", "/", -1) + byt, err := DoJs("getLayerSet.jsx", JSLayer(path)) + if err != nil { + log.Panic(err) + } + var out *LayerSet + err = json.Unmarshal(byt, &out) + if err != nil { + log.Println(JSLayer(path)) + log.Println(string(byt)) + log.Panic(err) + } + out.SetParent(g) + log.Printf("Loading ActiveDocument/%s\n", out.Path()) + if err != nil { + return &LayerSet{}, err + } + for _, lyr := range out.artLayers { + lyr.SetParent(out) + } + for i, set := range out.layerSets { + s, err := NewLayerSet(fmt.Sprintf("%s%s/", path, set.Name()), out) + if err != nil { + log.Fatal(err) + } + out.layerSets[i] = s + s.SetParent(out) + } + out.current = true + return out, err +} + +func (l *LayerSet) Visible() bool { + return l.visible +} + +// SetVisible makes the LayerSet visible. +func (l *LayerSet) SetVisible(b bool) { + if l.visible == b { + return + } + js := fmt.Sprintf("%s.visible=%v;", strings.TrimRight( + JSLayer(l.Path()), ";"), b) + DoJs("compilejs.jsx", js) + l.visible = b +} + +// SetPos snaps the given layerset boundry to the given point. +// Valid options for bound are: "TL", "TR", "BL", "BR" +func (l *LayerSet) SetPos(x, y int, bound string) { + if !l.visible || (x == 0 && y == 0) { + return + } + byt, err := DoJs("LayerSetBounds.jsx", JSLayer(l.Path()), + JSLayer(l.Path(), true)) + if err != nil { + log.Println(string(byt)) + log.Panic(err) + } + var bnds *[2][2]int + err = json.Unmarshal(byt, &bnds) + if err != nil { + fmt.Println(string(byt)) + log.Panic(err) + } + l.bounds = *bnds + var lyrX, lyrY int + switch bound[:1] { + case "B": + lyrY = l.bounds[1][1] + case "T": + fallthrough + default: + lyrY = l.bounds[0][1] + } + switch bound[1:] { + case "R": + lyrX = l.bounds[1][0] + case "L": + fallthrough + default: + lyrX = l.bounds[0][0] + } + byt, err = DoJs("moveLayer.jsx", JSLayer(l.Path()), fmt.Sprint(x-lyrX), + fmt.Sprint(y-lyrY), JSLayer(l.Path(), true)) + if err != nil { + fmt.Println("byte:", string(byt)) + panic(err) + } + var lyr LayerSet + err = json.Unmarshal(byt, &lyr) + if err != nil { + fmt.Println("byte:", string(byt)) + log.Panic(err) + } + l.bounds = lyr.bounds +} + +func (l *LayerSet) Refresh() { + var tmp *LayerSet + byt, err := DoJs("getLayerSet.jsx", JSLayer(l.Path()), JSLayer(l.Path(), true)) + err = json.Unmarshal(byt, &tmp) + if err != nil { + log.Println("Error in LayerSet.Refresh() \"", string(byt), "\"", "for", l.Path()) + log.Panic(err) + } + tmp.SetParent(l.Parent()) + for _, lyr := range l.artLayers { + err := lyr.Refresh() + if err != nil { + l.artLayers = tmp.artLayers + break + } + } + for _, set := range l.layerSets { + set.Refresh() + } + l.name = tmp.name + l.bounds = tmp.bounds + l.visible = tmp.visible + l.current = true +} diff --git a/win/dojs.vbs b/win/dojs.vbs deleted file mode 100644 index 222155b..0000000 --- a/win/dojs.vbs +++ /dev/null @@ -1,10 +0,0 @@ - -Dim app -Set app = CreateObject("Photoshop.Application") -if WScript.Arguments.Count = 0 then - WScript.Echo "Missing parameters" -else - path = wScript.Arguments(0) - folder = wScript.Arguments(1) - app.DoJavaScriptFile path, Array(Folder) -end if \ No newline at end of file diff --git a/win/test.jsx b/win/test.jsx deleted file mode 100644 index 98d79ac..0000000 --- a/win/test.jsx +++ /dev/null @@ -1,11 +0,0 @@ -var Path = arguments[0]; -alert(Path) -var saveFile = File(Path + "/test.txt"); - -if(saveFile.exists) - saveFile.remove(); - -saveFile.encoding = "UTF8"; -saveFile.open("e", "TEXT", "????"); -saveFile.writeln("Testing..."); -saveFile.close(); \ No newline at end of file diff --git a/win/test.txt b/win/test.txt deleted file mode 100644 index 5862d9e..0000000 --- a/win/test.txt +++ /dev/null @@ -1 +0,0 @@ -Testing... diff --git a/win/test.vbs b/win/test.vbs deleted file mode 100644 index 12d7bf1..0000000 --- a/win/test.vbs +++ /dev/null @@ -1 +0,0 @@ -wScript.echo "Testing..." \ No newline at end of file