imsg-grep 0.1.2-darwin
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +95 -0
- data/bin/img2png +0 -0
- data/bin/imsg-grep +749 -0
- data/bin/msg-info +133 -0
- data/bin/sql-shell +25 -0
- data/doc/HELP +181 -0
- data/doc/HELP_DATES +72 -0
- data/ext/extconf.rb +97 -0
- data/ext/img2png.swift +325 -0
- data/lib/imsg-grep/VERSION +1 -0
- data/lib/imsg-grep/apple/attr_str.rb +65 -0
- data/lib/imsg-grep/apple/bplist.rb +257 -0
- data/lib/imsg-grep/apple/keyed_archive.rb +105 -0
- data/lib/imsg-grep/dev/print_query.rb +84 -0
- data/lib/imsg-grep/dev/timer.rb +38 -0
- data/lib/imsg-grep/images/imaginator.rb +135 -0
- data/lib/imsg-grep/images/img2png.dylib +0 -0
- data/lib/imsg-grep/images/img2png.rb +84 -0
- data/lib/imsg-grep/messages.rb +314 -0
- data/lib/imsg-grep/utils/date.rb +117 -0
- data/lib/imsg-grep/utils/strop_utils.rb +79 -0
- metadata +161 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# NSKeyedArchive decoder for unpacking Apple's keyed archive format from binary plists
|
|
5
|
+
|
|
6
|
+
require "json"
|
|
7
|
+
require "base64"
|
|
8
|
+
require_relative "bplist"
|
|
9
|
+
|
|
10
|
+
class KeyedArchive
|
|
11
|
+
attr_reader :data
|
|
12
|
+
|
|
13
|
+
class BinaryString < String
|
|
14
|
+
def initialize(s) = super(s).force_encoding("BINARY")
|
|
15
|
+
def to_json(...) = Base64.strict_encode64(self).to_json(...)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.unarchive(...) = new(...).unarchive
|
|
19
|
+
def self.json(...) = new(...).json
|
|
20
|
+
|
|
21
|
+
def initialize(data)
|
|
22
|
+
return unless data
|
|
23
|
+
return if data[0, 4] == "\x08\x08\x11\x00" # DigitalTouchBalloonProvider row; another format, ignore
|
|
24
|
+
raise "no bplist: #{data}" unless data.start_with?("bplist")
|
|
25
|
+
@data = BPList.parse data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def unarchive
|
|
29
|
+
return unless @data # initialize failed
|
|
30
|
+
top = @data["$top"] or raise "no top"
|
|
31
|
+
root = top["root"] or raise "no root"
|
|
32
|
+
objs = @data["$objects"] or raise "no objects"
|
|
33
|
+
decode_objects dereference_uids(root, objs)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def json = unarchive&.to_json
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def dereference_uids(obj, objects, visited = Set.new, depth = 0)
|
|
41
|
+
# Prevent infinite recursion
|
|
42
|
+
raise "Maximum recursion depth exceeded" if depth > 1000
|
|
43
|
+
case obj
|
|
44
|
+
when Hash
|
|
45
|
+
if obj.size == 1 && obj.has_key?("CF$UID")
|
|
46
|
+
# This is a UID reference, expand it
|
|
47
|
+
uid = obj["CF$UID"]
|
|
48
|
+
return obj if visited.include?(uid) # Prevent infinite recursion
|
|
49
|
+
return obj if uid >= objects.length || uid < 0
|
|
50
|
+
|
|
51
|
+
visited.add(uid)
|
|
52
|
+
result = dereference_uids(objects[uid], objects, visited, depth + 1)
|
|
53
|
+
visited.delete(uid)
|
|
54
|
+
return result
|
|
55
|
+
else
|
|
56
|
+
# Regular hash, expand all values
|
|
57
|
+
result = {}
|
|
58
|
+
obj.each { |k, v| result[k] = dereference_uids(v, objects, visited, depth + 1) }
|
|
59
|
+
return result
|
|
60
|
+
end
|
|
61
|
+
when Array
|
|
62
|
+
obj.map { |item| dereference_uids(item, objects, visited, depth + 1) }
|
|
63
|
+
else
|
|
64
|
+
obj
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def decode_objects(obj)
|
|
69
|
+
case obj
|
|
70
|
+
when Hash
|
|
71
|
+
case obj.dig("$class", "$classname")
|
|
72
|
+
in "NSArray" if obj.key? "NS.objects"
|
|
73
|
+
obj["NS.objects"].map { |item| decode_objects(item) }
|
|
74
|
+
|
|
75
|
+
in "NSDictionary" if obj.key?("NS.keys") && obj.key?("NS.objects")
|
|
76
|
+
obj["NS.keys"].zip(obj["NS.objects"].map{ decode_objects it }).to_h
|
|
77
|
+
|
|
78
|
+
in "NSURL" if obj["NS.base"] == "$null" && obj["NS.relative"]
|
|
79
|
+
obj["NS.relative"]
|
|
80
|
+
else
|
|
81
|
+
obj.transform_values { decode_objects it }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
when Array then obj.map { |item| decode_objects(item) }
|
|
85
|
+
when "$null" then nil # Transform $null strings to actual null
|
|
86
|
+
|
|
87
|
+
when String # Transform binary data to Base64 ; detect if string is binary (ASCII-8BIT) or invalid UTF-8
|
|
88
|
+
case
|
|
89
|
+
when obj.encoding == Encoding::BINARY then BinaryString.new(obj)
|
|
90
|
+
when obj.force_encoding('UTF-8').valid_encoding? then obj
|
|
91
|
+
else BinaryString.new(obj)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
else obj
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __FILE__ == $0
|
|
102
|
+
input = $stdin.read
|
|
103
|
+
out = KeyedArchive.unarchive(input)
|
|
104
|
+
puts JSON.pretty_generate(out)
|
|
105
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Query result formatter that prints SQL results in Very Nice tables
|
|
4
|
+
|
|
5
|
+
require "io/console"
|
|
6
|
+
|
|
7
|
+
module Print
|
|
8
|
+
@term_width = IO.console.winsize[1]
|
|
9
|
+
@term_width -= 3 if ENV["TERM_PROGRAM"] == "zed" # safe zone for https://github.com/zed-industries/zed/issues/43629
|
|
10
|
+
|
|
11
|
+
def self.c256(c, s) = "\e[38;5;#{c}m#{s}\e[39m"
|
|
12
|
+
def self.mark(s) = "\e[38;5;178m#{s}\e[39m"
|
|
13
|
+
def self.sep(s) = "\e[30m#{s}\e[39m"
|
|
14
|
+
def self.clr = "\e[0m"
|
|
15
|
+
|
|
16
|
+
def self.query(q, *args, title: nil, db: Messages.db)
|
|
17
|
+
table db.execute2(q, *args), title:
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.table(table, title: nil)
|
|
21
|
+
bg1 = "\e[48;5;4m"
|
|
22
|
+
bga = ["\e[48;5;235m", "\e[48;5;238m"]
|
|
23
|
+
bg = ->{ bg1 ? (bg1.tap{bg1=nil}) : bga.rotate!.first }
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# puts "\n#{'=' * 80}"
|
|
27
|
+
hrow, *rows = table
|
|
28
|
+
# max lengths for each col, but capping the header rows at 5 so big headers with short content don't take up space
|
|
29
|
+
lens = [hrow.map{it[0,5]}].concat(rows).transpose.map{|col|col.map{it.to_s.size}.max||0}
|
|
30
|
+
# is each column number? used for aligning them right
|
|
31
|
+
nums = rows.transpose.map{|col| col.compact.all?{ Numeric===it }}
|
|
32
|
+
hex = rows.transpose.map{|col| col.compact.all?{ String===it && it !~ /\H/ }}
|
|
33
|
+
hex.zip(0..).each{|h,i| lens[i] = 20 if h } # hex cols start short
|
|
34
|
+
|
|
35
|
+
# available term width for all cols excluding separators
|
|
36
|
+
colsw = @term_width - hrow.size + 1
|
|
37
|
+
# max width per col; start with an even col width then redistribute next
|
|
38
|
+
maxcw = colsw / hrow.size
|
|
39
|
+
|
|
40
|
+
# begin redistributing
|
|
41
|
+
bigs = lens
|
|
42
|
+
taken = 0
|
|
43
|
+
while true
|
|
44
|
+
smalls, bigs = bigs.partition{ it <= maxcw } # partition cols under/at max col width
|
|
45
|
+
break if bigs.empty? || smalls.empty? # can't redistribute if all on either side
|
|
46
|
+
taken += smalls.sum # accumulate used up width
|
|
47
|
+
maxcw = (colsw - taken) / bigs.size # max for remaining bigs: split even
|
|
48
|
+
end
|
|
49
|
+
lens.map!{ [it, maxcw].min }
|
|
50
|
+
|
|
51
|
+
# expand col header width while fits; those we capped earlier
|
|
52
|
+
trunc_headers = lens.zip(hrow.map{it.size}, 0..).filter_map{|l,hl,i| [i,hl] if l<hl } # [col index, full header length]
|
|
53
|
+
while lens.sum < colsw && trunc_headers.any? # while still fits and any left to widen
|
|
54
|
+
i, hl = trunc_headers.first # take first
|
|
55
|
+
next trunc_headers.shift if lens[i] >= hl # drop and nove on if wide enough
|
|
56
|
+
lens[i] += 1 # widen
|
|
57
|
+
trunc_headers.rotate! # move on to next index
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
total_width = lens.sum + hrow.size - 1 # full width table will take
|
|
61
|
+
|
|
62
|
+
row_count = "(#{rows.size} row#{?s if rows.size != 1})"
|
|
63
|
+
title = [title, row_count].join(" ")
|
|
64
|
+
puts "\e[48;5;18m" + title.ljust(total_width) + "\e[49m"
|
|
65
|
+
|
|
66
|
+
lines = table.map do |row|
|
|
67
|
+
line = row.zip(lens, nums).map do |val, len, num|
|
|
68
|
+
just = ->s{ s.send(num ? :rjust : :ljust, len) }
|
|
69
|
+
next c256(244, just[?∅]) if val.nil? # special handling for nil
|
|
70
|
+
next c256(112, just["1"]) if val == 1 # color 1 bool true
|
|
71
|
+
next c256(9, just["0"]) if val == 0 # color 0 bool false
|
|
72
|
+
s ||= val.to_s
|
|
73
|
+
s = s.gsub(/\p{Extended_Pictographic}/, "•").gsub(/\R/, "␍").gsub(/\t/, ' ') # remove emojis, nl, tabs (better chance of not mangling columns)
|
|
74
|
+
s = s[0, len-1] + mark(?›) if s.size > len # ellipsis
|
|
75
|
+
just[s]
|
|
76
|
+
end
|
|
77
|
+
bg[] + line*sep(?│) + clr
|
|
78
|
+
end
|
|
79
|
+
puts lines
|
|
80
|
+
puts "\e[48;5;18m" + row_count.ljust(total_width) + "\e[49m"
|
|
81
|
+
puts
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Simple timer for profiling with lap times and progress bars
|
|
4
|
+
module Timer
|
|
5
|
+
# Initialize timer state
|
|
6
|
+
def self.start
|
|
7
|
+
@t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
8
|
+
@last_lap = @t0
|
|
9
|
+
@total_time = 0.0
|
|
10
|
+
@laps = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Record lap time with message
|
|
14
|
+
def self.lap(msg)
|
|
15
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
16
|
+
lap_time = (now - @last_lap) * 1000
|
|
17
|
+
@total_time += lap_time
|
|
18
|
+
line = "%5.0fms / %5.0fms: #{msg}" % [lap_time, @total_time]
|
|
19
|
+
$stderr.puts line
|
|
20
|
+
@laps << { msg: msg, time: lap_time, line: line, line_num: @laps.size }
|
|
21
|
+
@last_lap = now
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Display final timing report with bars and percentages
|
|
25
|
+
def self.finish
|
|
26
|
+
max_lap_time = @laps.map { |lap| lap[:time] }.max
|
|
27
|
+
longest_line = @laps.map { |lap| lap[:line].length }.max
|
|
28
|
+
start_col = longest_line + 3
|
|
29
|
+
@laps.reverse.each do |lap|
|
|
30
|
+
pct = "%4.1f" % (lap[:time] / @total_time * 100)
|
|
31
|
+
bar_length = (lap[:time] / max_lap_time * 20).round
|
|
32
|
+
bar = "█" * bar_length + "░" * (20 - bar_length)
|
|
33
|
+
padding = " " * [0, start_col - lap[:line].length].max
|
|
34
|
+
$stderr.print "\e[#{@laps.size - lap[:line_num]}A\r#{lap[:line]}#{padding}#{bar} #{pct}%\e[#{@laps.size - lap[:line_num]}B\r"
|
|
35
|
+
end
|
|
36
|
+
$stderr.puts
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
require "io/console"
|
|
2
|
+
require "concurrent-ruby"
|
|
3
|
+
require_relative "img2png"
|
|
4
|
+
|
|
5
|
+
module Imaginator
|
|
6
|
+
begin
|
|
7
|
+
require_relative "img2png"
|
|
8
|
+
EXTENSION_AVAILABLE = true
|
|
9
|
+
rescue LoadError
|
|
10
|
+
EXTENSION_AVAILABLE = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
class Image
|
|
16
|
+
Fit = Data.define :w, :h, :c, :r, :pad_h, :pad_w
|
|
17
|
+
|
|
18
|
+
def initialize(path) = @path = path
|
|
19
|
+
def dimensions = @img.dimensions
|
|
20
|
+
def release = @img.release
|
|
21
|
+
|
|
22
|
+
def load
|
|
23
|
+
return unless File.exist? @path
|
|
24
|
+
# @img ||= Img2png::Image.new path: @path # do the the IO in swift, turns out not any faster
|
|
25
|
+
@img ||= Img2png::Image.new data: IO.binread(@path)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def png_transform w:, h:, pad_h:nil, pad_w:nil
|
|
30
|
+
pad = pad_h || pad_w
|
|
31
|
+
pad_w ||= w if pad
|
|
32
|
+
pad_h ||= h if pad
|
|
33
|
+
@img.convert fit_w:w, fit_h:h, box_w:pad_w, box_h:pad_h
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fit_cols target_cols
|
|
37
|
+
w_cell, h_cell = Imaginator.cell_size
|
|
38
|
+
w_img, h_img = dimensions
|
|
39
|
+
w = target_cols * w_cell # target width in px
|
|
40
|
+
h = w / w_img * h_img # target height in px
|
|
41
|
+
r = (h / h_cell).ceil # target rows
|
|
42
|
+
Fit.new w:w.floor, h:h.floor, c:target_cols, r:r, pad_h:(r*h_cell), pad_w:w
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fit_rows target_rows
|
|
46
|
+
w_cell, h_cell = Imaginator.cell_size
|
|
47
|
+
w_img, h_img = dimensions
|
|
48
|
+
h = target_rows * h_cell # target height in px
|
|
49
|
+
w = h / h_img * w_img # target width in px
|
|
50
|
+
c = (w / w_cell).ceil # target cols
|
|
51
|
+
Fit.new w:w.floor, h:h.floor, c:c, r:target_rows, pad_w:(c*w_cell), pad_h:h
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fit cols, rows
|
|
55
|
+
cfit = fit_cols(cols)
|
|
56
|
+
rfit = fit_rows(rows)
|
|
57
|
+
smol = [cfit, rfit].sort_by { [it.w, it.h] }.first
|
|
58
|
+
# ^ they're proportional so whichever smallest is the one containable in both dimensions
|
|
59
|
+
[smol, cfit:, rfit:]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def term_seq(seq, end_marker)
|
|
66
|
+
t = ->{ Process.clock_gettime Process::CLOCK_MONOTONIC }
|
|
67
|
+
buf = ""
|
|
68
|
+
IO.console.raw do |tty|
|
|
69
|
+
tty << seq
|
|
70
|
+
tty.flush
|
|
71
|
+
timeout = t.() + 0.1
|
|
72
|
+
loop do
|
|
73
|
+
buf << tty.read_nonblock(1)
|
|
74
|
+
break if buf.end_with? end_marker
|
|
75
|
+
rescue IO::WaitReadable
|
|
76
|
+
break if t.() > timeout
|
|
77
|
+
IO.select([tty], nil, nil, 0.01)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
buf
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def term_features = @term_features ||= (term_seq("\e]1337;Capabilities\e\\", ?\a) =~ /Capabilities=([A-Za-z0-9]*)/ and $1.scan(/[A-Z][a-z]?\d*/))
|
|
84
|
+
def iterm_images? = @iterm_images ||= term_features&.include?(?F)
|
|
85
|
+
def kitty_images? = @kitty_images ||= term_seq("\e_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\e\\", "\e\\") == "\e_Gi=31;OK\e\\"
|
|
86
|
+
|
|
87
|
+
def cell_size # [cell width px, cell height px] (Floats)
|
|
88
|
+
@cell_size ||= case
|
|
89
|
+
when iterm = term_seq("\e]1337;ReportCellSize\e\\", ?\a)[/ReportCellSize=(.*)\e/, 1]
|
|
90
|
+
iterm.split(?;).map{ Float it }.then{ |h, w, s| s ||= 1; [w*s, h*s] } # multiply by retina scale factor
|
|
91
|
+
when csi16t = term_seq("\e[16t", ?t)[/\e\[6;(\d+;\d+)t/, 1] # ghostty
|
|
92
|
+
csi16t.split(?;).map{ Float it }.reverse
|
|
93
|
+
# else calc from TIOCGWINSZ or CSI14/18t, but this is enough for iterm, ghostty
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def term_image_protocol
|
|
98
|
+
return @term_image_protocol unless @term_image_protocol.nil?
|
|
99
|
+
@term_image_protocol = case
|
|
100
|
+
when ENV["TERM_PROGRAM"] == "Apple_Terminal" then false # it echoes back the kitty query and I don't wanna figure out how not to
|
|
101
|
+
when iterm_images? then :iterm # has to go before kitty as iterm responds ok to kitty query but can't render them
|
|
102
|
+
when kitty_images? then :kitty # you're a kitty! yes you are! and you're sitting there! hi, kitty!
|
|
103
|
+
else false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def image_tooling? = !!(EXTENSION_AVAILABLE && term_image_protocol && cell_size)
|
|
108
|
+
|
|
109
|
+
def iterm_print_image data:, r:nil, c:nil, io:$stdout
|
|
110
|
+
head = "\e]1337;"
|
|
111
|
+
io << head << "MultipartFile=inline=1;preserveAspectRatio=1"
|
|
112
|
+
io << ";width=" << c if c
|
|
113
|
+
io << ";height=" << r if r
|
|
114
|
+
io << ?\a
|
|
115
|
+
data = [data].pack("m0")
|
|
116
|
+
(0...data.size).step(200).each{ io << head << "FilePart=" << data[it, 200] << ?\a }
|
|
117
|
+
io << head << "FileEnd" << ?\a
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def kitty_print_image data:, r:nil, c:nil, io:$stdout
|
|
121
|
+
io << "\e_Gf=100,a=T,t=d"
|
|
122
|
+
io << ",c=" << c if c
|
|
123
|
+
io << ",r=" << r if r
|
|
124
|
+
io << ?; << [data].pack("m0") << "\e\\"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def print_image(...)
|
|
128
|
+
return unless image_tooling?
|
|
129
|
+
case term_image_protocol
|
|
130
|
+
when :iterm then iterm_print_image(...)
|
|
131
|
+
when :kitty then kitty_print_image(...)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "ffi"
|
|
2
|
+
|
|
3
|
+
module Img2png
|
|
4
|
+
extend FFI::Library
|
|
5
|
+
ffi_lib "#{__dir__}/img2png.dylib"
|
|
6
|
+
|
|
7
|
+
attach_function :img2png_load_path, [:string, :pointer, :pointer], :pointer, blocking: true
|
|
8
|
+
attach_function :img2png_load, [:pointer, :int, :pointer, :pointer], :pointer, blocking: true
|
|
9
|
+
attach_function :img2png_convert, [:pointer, :int, :int, :int, :int, :pointer, :pointer], :bool, blocking: true
|
|
10
|
+
attach_function :img2png_release, [:pointer], :void
|
|
11
|
+
attach_function :img2png_free, [:pointer], :void
|
|
12
|
+
|
|
13
|
+
class Image
|
|
14
|
+
def initialize(path:nil, data:nil)
|
|
15
|
+
[path, data].compact.size == 1 or raise ArgumentError, "One of path: or data: must be specified"
|
|
16
|
+
out_w = FFI::MemoryPointer.new(:int)
|
|
17
|
+
out_h = FFI::MemoryPointer.new(:int)
|
|
18
|
+
|
|
19
|
+
@handle = case
|
|
20
|
+
when data then Img2png.img2png_load(data, data.bytesize, out_w, out_h)
|
|
21
|
+
when path then Img2png.img2png_load_path(path, out_w, out_h)
|
|
22
|
+
end
|
|
23
|
+
raise "Failed to load image" if @handle.null?
|
|
24
|
+
|
|
25
|
+
@width = out_w.read_int
|
|
26
|
+
@height = out_h.read_int
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dimensions = [@width, @height]
|
|
30
|
+
|
|
31
|
+
def convert(fit_w: nil, fit_h: nil, box_w: nil, box_h: nil)
|
|
32
|
+
out_data = FFI::MemoryPointer.new(:pointer)
|
|
33
|
+
out_len = FFI::MemoryPointer.new(:int)
|
|
34
|
+
|
|
35
|
+
success = Img2png.img2png_convert(
|
|
36
|
+
@handle,
|
|
37
|
+
fit_w || 0, fit_h || 0,
|
|
38
|
+
box_w || 0, box_h || 0,
|
|
39
|
+
out_data, out_len
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return nil unless success
|
|
43
|
+
|
|
44
|
+
ptr = out_data.read_pointer
|
|
45
|
+
len = out_len.read_int
|
|
46
|
+
result = ptr.read_bytes(len)
|
|
47
|
+
Img2png.img2png_free(ptr)
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def release
|
|
52
|
+
Img2png.img2png_release(@handle) unless @handle.null?
|
|
53
|
+
@handle = FFI::Pointer::NULL
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.finalize(handle)
|
|
57
|
+
proc { Img2png.img2png_release(handle) unless handle.null? }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# # Usage examples
|
|
63
|
+
# input = File.binread("x.heic")
|
|
64
|
+
|
|
65
|
+
# # Load once, use multiple times
|
|
66
|
+
# img = Img2png::Image.new(input)
|
|
67
|
+
|
|
68
|
+
# # Get dimensions
|
|
69
|
+
# p img.dimensions # => [4032, 3024]
|
|
70
|
+
|
|
71
|
+
# # Convert without scaling
|
|
72
|
+
# png = img.convert
|
|
73
|
+
# File.binwrite("output.png", png)
|
|
74
|
+
|
|
75
|
+
# # Fit to 800x600
|
|
76
|
+
# png = img.convert(fit_w: 800, fit_h: 600)
|
|
77
|
+
# File.binwrite("fitted.png", png)
|
|
78
|
+
|
|
79
|
+
# # Fit to 800x600 and box in 1024x768
|
|
80
|
+
# png = img.convert(fit_w: 800, fit_h: 600, box_w: 1024, box_h: 768)
|
|
81
|
+
# File.binwrite("boxed.png", png)
|
|
82
|
+
|
|
83
|
+
# # Manual cleanup (or let GC handle it)
|
|
84
|
+
# img.release
|