aboutsummaryrefslogtreecommitdiff
path: root/Wallgen.fsx
blob: 24e72ad8607274b2e7b4403b93ac4ec1cfe81024 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#!/usr/bin/env -S dotnet fsi
#r "nuget: Argu, 6.2.5"
#r "nuget: Tommy, 3.1.2"
open Argu
open System
open System.IO
open Tommy
open System.Diagnostics
open System.Threading.Tasks

type Argument =
    | [<Mandatory; AltCommandLine("-i")>] Inputs of input: string list
    | [<Mandatory; AltCommandLine("-t")>] Texts of text: string list
    | [<Mandatory; Unique; AltCommandLine("-o")>] Output of output: string
    | [<Unique; AltCommandLine("-c")>] Config of config: string
    | [<Unique; AltCommandLine("-f")>] Format of format: string

    interface IArgParserTemplate with
        member s.Usage : string = 
            match s with
            | Inputs _ -> "input images"
            | Texts _ -> "texts to apply to input images"
            | Output _ -> "directory to place output images"
            | Config _ -> "configuration file location (default: config.toml)"
            | Format _ -> "output image format (file extension; by default reuse format of input image)"

type TextOption =
    | Font of string
    | FontSize of uint
    | MaxWidth of uint
    | Color of string
    | Background of string

let generateImage
    (input : string)
    (text : string)
    (options : TextOption list)
    (output : string)
    =
    let args = 
        [ input ]
        @ (options
            |> List.map (fun o ->
                match o with
                | Font font -> "font", font
                | FontSize ps -> "pointsize", string(ps)
                | MaxWidth w -> "size", string(w)
                | Color color -> "fill", color
                | Background bg -> "undercolor", bg
            )
            |> List.map (fun (param, value) -> [ sprintf "-%s" param; value ])
            |> List.concat)
        @ [ "-background"; "transparent" ]
        @ [ "-gravity"; "center" ]
        @ [ "caption:@-" ]
        @ [ "-composite" ]
        @ [ output ]
    printf "%s" (sprintf "magick %s\n" (args |> String.concat " ")) // There's a race for stdout
    let startInfo =
        ProcessStartInfo("magick", args,
            RedirectStandardInput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        )
    use p = Process.Start(startInfo)

    text.Split('\n')
    |> Array.map (fun s ->
        match s.Trim() with
        | "" -> ""
        | s -> " " + s + " "
    )
    |> fun parts -> String.Join('\n', parts)
    |> p.StandardInput.Write
    p.StandardInput.Close()
    p.WaitForExit()

let parser = ArgumentParser.Create<Argument>()
let results = 
    try
        parser.ParseCommandLine(Environment.GetCommandLineArgs() |> Array.skip 2)
    with
        | :? ArguParseException as exn ->
            printfn "%s" exn.Message
            exit 1

let inputs =
    results.GetResults(Inputs)
    |> List.reduce (fun a b -> List.append a b)

let texts =
    results.GetResults(Texts)
    |> List.reduce (fun a b -> List.append a b)

let output = results.GetResult(Output)

let configFile = results.TryGetResult(Config) |> Option.defaultValue "config.toml"

let format = results.TryGetResult(Format)

let readConfig file : TextOption list * Map<string, TextOption list> =
    let parseSection (defaults : Map<string, TextOption> option) (table : TomlTable) : Map<string, TextOption> * string list =
        let values = defaultArg defaults (Map [])

        ((values, []), table.RawTable |> Seq.filter (fun kvp -> not kvp.Value.IsTable))
        ||> Seq.fold (fun (values, errors) kvp ->
            let key, value = kvp.Key, kvp.Value

            let str f =
                if value.IsString then
                    values.Add(key, (f value.AsString.Value)), errors
                else
                    values, [ sprintf "'%s' must be string" key ] @ errors
            let uint f =
                if value.IsInteger then
                    let i = value.AsInteger.Value
                    if i >= 0 then
                        values.Add(key, f ((uint)i)), errors
                    else
                        values, [ sprintf "'%s' must be positive integer" key ] @ errors
                else
                    values, [ sprintf "'%s' must be positive integer" key ] @ errors

            let key = kvp.Key
            match key with
            | "font" -> str (fun s -> Font s)
            | "font_size" -> uint (fun i -> FontSize i)
            | "max_width" -> uint (fun i -> MaxWidth i)
            | "color" -> str (fun s -> Color s)
            | "background" -> str (fun s -> Background s)
            | k -> values, [ sprintf "unrecognized key '%s'" k ] @ errors
        )

    let convert (s : Map<string, TextOption>) : TextOption list =
        s
        |> Seq.map (fun x -> x.Value)
        |> Seq.rev
        |> List.ofSeq

    let toml =
        try
            use reader = File.OpenText(file)
            TOML.Parse(reader)
        with
            | :? FileNotFoundException ->
                printfn "config file %s not found" file
                exit 1
            | :? TomlParseException as ex ->
                for se in ex.SyntaxErrors do
                    printfn "%s:%d:%d: %s" file se.Line se.Column se.Message
                exit 1

    let def : Map<string, TextOption> =
        match parseSection None toml with
        | def, [] -> def
        | _, errors ->
            for error in errors do
                printfn "%s: failed to parse default configuration: %s" file error
            exit 1

    let images =
        (([], []), toml.RawTable |> Seq.filter (fun kvp -> kvp.Value.IsTable))
        ||> Seq.fold (fun state kvp ->
            match parseSection (Some def) kvp.Value.AsTable with
            | values, [] -> [ (kvp.Key, values) ] @ fst state, snd state
            | _, errors -> fst state, [ (kvp.Key, errors) ] @ snd state
        )
        |> function
            | images, [] ->
                Seq.map (fun (section, values) -> section, convert values) images |> Map
            | _, errors ->
                for (section, errors) in errors do
                    for error in errors do
                        printfn "%s: failed to parse section %s: %s" file section error
                exit 1

    convert def, images

let defaultConfig, imageConfigs = readConfig configFile

match List.filter (fun i -> not (File.Exists i)) inputs with
| [] -> ()
| notFound ->
    for f in notFound do
        printfn "input file '%s' not found" f
    exit 1

match List.filter (fun t -> not (File.Exists t)) texts with
| [] -> ()
| notFound ->
    for f in notFound do
        printfn "text source '%s' not found" f
    exit 1

try
    Directory.CreateDirectory(output) |> ignore
with
    | ex ->
        printfn "failed to create output directory '%s': %s" output ex.Message
        exit 1

let taggedTexts =
    texts
    |> List.map (fun t ->
        Path.GetFileName(t), File.ReadAllText(t)
    )

Parallel.ForEach(
    (inputs |> List.map (fun i -> taggedTexts |> List.map (fun t -> i, t)) |> List.concat),
    (fun (input : string, (tag, text)) ->
        let name = Path.GetFileNameWithoutExtension(input)
        let options =
            imageConfigs.TryFind(name)
            |> Option.defaultValue defaultConfig
        let format = format |> Option.defaultValue (Path.GetExtension(input))
        let output = sprintf "%s/%s-%s%s" output name (tag) format
        
        generateImage input text options output
    ))
|> ignore