From cc8e85784973ca3a5c7dd971698480abaf1ad42e Mon Sep 17 00:00:00 2001 From: Charlie Jiang Date: Thu, 9 May 2019 16:26:08 -0400 Subject: [PATCH 01/10] Merge remote-tracking branch 'origind-dev/master' Add align to video function --- src/bitmaps/button/button_align_16.png | Bin 0 -> 353 bytes src/bitmaps/button/button_align_24.png | Bin 0 -> 571 bytes src/bitmaps/button/button_align_32.png | Bin 0 -> 690 bytes src/bitmaps/button/button_align_48.png | Bin 0 -> 1161 bytes src/bitmaps/button/button_align_64.png | Bin 0 -> 547 bytes src/bitmaps/manifest.respack | 5 + src/colour_button.h | 6 + src/command/time.cpp | 13 + src/dialog_align.cpp | 378 +++++++++++++++++++++++++ src/dialogs.h | 1 + src/hotkey.cpp | 10 + src/image_position_picker.cpp | 127 +++++++++ src/image_position_picker.h | 37 +++ src/libresrc/default_config.json | 4 + src/libresrc/default_hotkey.json | 3 + src/libresrc/default_toolbar.json | 1 + src/meson.build | 2 + 17 files changed, 587 insertions(+) create mode 100644 src/bitmaps/button/button_align_16.png create mode 100644 src/bitmaps/button/button_align_24.png create mode 100644 src/bitmaps/button/button_align_32.png create mode 100644 src/bitmaps/button/button_align_48.png create mode 100644 src/bitmaps/button/button_align_64.png create mode 100644 src/dialog_align.cpp create mode 100644 src/image_position_picker.cpp create mode 100644 src/image_position_picker.h diff --git a/src/bitmaps/button/button_align_16.png b/src/bitmaps/button/button_align_16.png new file mode 100644 index 0000000000000000000000000000000000000000..07ce86c1b127a1cb5711bebf1b67ccb99b70623d GIT binary patch literal 353 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!Ec@}jv*HQ$$$R;w`bOD;PevaWeYoeTf2S7 z&Ykb;+KUt!7AA(JswnepIeV+>^|!T;kN4OA{`U66%IE-50R|pbjvwEDzn$|m`hLtk zRuK{L$&DwP7_CECB@|3@W{0s&xwAFZaDq+I%PZ0gE;KK8_?FyZ!lPiq(73%%BA|6k zZbeV43Ul*?)34nd?i9aYxJjU4(Zj<}-#UnXyt=s|M?qOxS$ch&z5I=aPYDH&9_mjx zVLO30{eEL=Vgu_UCXQcPm+B1-YX1I@pEt4a`90Cw`FHl!l(7fQnJlZs&m7R}@Zssf xZ^0b$Ru4^P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0nkZAK~y+TWBmXB zKLa%Y6O^VHj2c83kYLdD-Ag>&-Q#MfLiwlwJ^FVoo4o((D|S|9Ap8HnzyDaon(J~Q zQYU5>TW0hz7)AX*yA&dV0)Pe~gLor$s00*k3&>?PjfQa1 z0J=dHLE1po2~BH(>|#ezUZdbozwWCF$rza!^MO)3gM?!$L?s%)2<+P%ymIG3*)X69 zG64d3cis976-5O^6}5kU|M>O$KQl8UJ39wd2m_3k*@OTC0{}7T!*frBZ>RtO002ov JPDHLkV1fYY?n3|o literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/button_align_32.png b/src/bitmaps/button/button_align_32.png new file mode 100644 index 0000000000000000000000000000000000000000..958f4b34eb382f97943c26277c79d85bcf642221 GIT binary patch literal 690 zcmV;j0!{siP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0!K+iK~zXfWBmXB zKLafR6O^VI7`1>P7`1>PAU_{m+B$d1k^4-4zIkM~J1DS2MKJ-;0`kDT1U-le6}#U< zg|GqaL8QWn1*pc%hf0HifPerR11rEDM7oTOSD;J;un8&iHsty7>o1Va#>stl&E$1w z9s}8S(M8^FE@g3^U{&Y3CRl{!Pj`>sGmumY@+EWWS91I#!QxAfYz3zxubi4fIS62Q1t z&jxcD-hKHFVSIZ13Bte*pcY{O4^F582;lN7F4lsw-MbC~6)efLf^Zx5e}rhk3h?BE zgAn}?kg@pb|NpNTp*#kl4Vw#WA#D5>;3+EXcCON%? zfw1udd}Yyw`DdUkhF8;%f++j)?Vj8qM%mU)ii~)J9uA;B!2sK9ETAf2;4xVI^NA%} z@B9NYD2jtaJ>C$_C_pa0eYiLs$Xf==*qB8W!`dehRoDS`3%>5HW?-=X1LdIrUooh; zP@s_mmB0aTR5*XXFfj8#*{I;?x~ZGbykKKt6cpsw(e`n5Q-@060HZBCf?(7F0)T-5 Y0C1xRlIB)rD*ylh07*qoM6N<$g5zf=@Bjb+ literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/button_align_48.png b/src/bitmaps/button/button_align_48.png new file mode 100644 index 0000000000000000000000000000000000000000..9d28cffad34b14a970c6f66148f86730efd2ce38 GIT binary patch literal 1161 zcmV;41a|w0P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1RhC5K~z{r?U;K^ zQ&$|vPfK4`sP+QoQ64@L5g97Vj0*;$GDwWkKNMfVY|b%_pvGlWnHq7~{-DMfW}I78 zW;9!tiLV$nQ9(tGbR*yc4WNP&)bNx}Ob_MBNo;}?uUUPNE^xt=3ayd zb5talLc;ax5HJzagmGX=fKk*9@o?gT9c8HGZJissyF}m6d~Kw zJ9d~W!ZPoAJv;DS*1Yvq4u}ytU%K32BGlj40hSa%*ne|+Z4{&;WXxQCzGCkfEw~8H zxQ#o$o;r-T6D-wlXX0)_lJB^fyt^5~5|C2V_LVU5dlc<2n+&EG{iKu4PQFWU1>xSS zmPnDcRTiG>#qB53L%}Bun`q&C1X&QN?%%&khvDC1%)Ro&McS$?^MrTR+ zK^XT`&rPAV0?b&q{}pX=H!VmL%o5_t&Q=yp^zE`i36gd;5V(xIBs17v)TX$2JvBqd zRUF?W5NmtX^4NiWciG?oVJw=DY!u4jnSQJjS_!8<8%?97_5YH?d`)sNm+aN8Kc*O9 z^moZ@g%s@Wx}5*%B5ObO?gg``Z8b9!VCeTrFFfO>X&(AP`Tx6&RYN`37tfk~sSP|Y zI+gGNsP^YV%cUN#rQm%)qg6_-PCr1HD6aMu*?x~-$v@r3_}L!tDC6?#{4T53qe;S0ltFz*w@HvV39jtoo&e#{c|04npRSi^{L{wVztLqIk!S?b}OAzAxEYkTu$O zyC9T%Nb~|0hgCyu6^R4SpIKW@Ly*vMBZtII`BpovohvbrCBn8kK1CYrqP|(2N{*U2 zCDwPU7UOOkoQss=3Gns)q?ba5G$ib@+S0{a&Ux>MfK+~y2nu*eO^0)-^Ot;kp`Bj0 zQjzfH`r6?R(Fq;^#c_5&yw%#L;a@g99#2?Us8$mZm&grvv&nx8vycU`kOi@j1+f@n bf)IKGkv!)D1ej0e00000NkvXXu0mjfC*2^6 literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/button_align_64.png b/src/bitmaps/button/button_align_64.png new file mode 100644 index 0000000000000000000000000000000000000000..0d6039c0f20196b86105833560a4b72b2d7b5cb4 GIT binary patch literal 547 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGH$#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!h8>hl?nh|9`X&#%!?c$n)|uyNCYXKOc> zDyFTtd*;C1KU)txdmj<1xr^}}H}mn8bH!&Jvw67DA*V*GY?5f2zvu-`ms>T{Q_U21 zTh8KDxe%&Q8T2oEE{~Z{LwLgDE8V?J0S&%inM>}#D^y%x-#_qSDF!-3_{V+XBk1uUs|cX54Lyl*3O z&HwkO&OB8R=sT14@AUFrXUg3=OxEQUt}L*Z=Hpw_Vvy&YmiI`DH~344!9zhsT^2dX zJvSOO8}B?3Fw(u6vyVsqq5AqqzW>!(&sh|+y0g81be_vVSMCsR;4l88zV#BRK?$~K eVLq+)KjREVtEGO6wweGVmci52&t;ucLK6V2*xU;M literal 0 HcmV?d00001 diff --git a/src/bitmaps/manifest.respack b/src/bitmaps/manifest.respack index 2dc110d3a..55c731bd9 100644 --- a/src/bitmaps/manifest.respack +++ b/src/bitmaps/manifest.respack @@ -51,6 +51,11 @@ button/bugtracker_button_24.png button/bugtracker_button_32.png button/bugtracker_button_48.png button/bugtracker_button_64.png +button/button_align_16.png +button/button_align_24.png +button/button_align_32.png +button/button_align_48.png +button/button_align_64.png button/button_audio_commit_16.png button/button_audio_commit_24.png button/button_audio_commit_32.png diff --git a/src/colour_button.h b/src/colour_button.h index b2e0f9335..6234dc9fc 100644 --- a/src/colour_button.h +++ b/src/colour_button.h @@ -43,6 +43,12 @@ public: /// Get the currently selected color agi::Color GetColor() { return colour; } + + void SetColor(agi::Color color) + { + colour = color; + UpdateBitmap(); + } }; struct ColorValidator final : public wxValidator { diff --git a/src/command/time.cpp b/src/command/time.cpp index d703ebdc0..eec19c2d4 100644 --- a/src/command/time.cpp +++ b/src/command/time.cpp @@ -235,6 +235,18 @@ struct time_snap_scene final : public validate_video_loaded { } }; +struct time_align_subtitle_to_point final : public validate_video_loaded { + CMD_NAME("time/align") + CMD_ICON(button_align) + STR_MENU("Align subtitle to video") + STR_DISP("Align subtitle to video") + STR_HELP("Align subtitle to video by key points.") + void operator()(agi::Context* c) override { + c->videoController->Stop(); + ShowAlignToVideoDialog(c); + } +}; + struct time_add_lead_both final : public Command { CMD_NAME("time/lead/both") STR_MENU("Add lead in and out") @@ -393,6 +405,7 @@ namespace cmd { reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); + reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); } diff --git a/src/dialog_align.cpp b/src/dialog_align.cpp new file mode 100644 index 000000000..004408bfa --- /dev/null +++ b/src/dialog_align.cpp @@ -0,0 +1,378 @@ +// Copyright (c) 2019, Charlie Jiang +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the Aegisub Group nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Aegisub Project http://www.aegisub.org/ + +#include "ass_dialogue.h" +#include "ass_file.h" +#include "compat.h" +#include "dialog_manager.h" +#include "format.h" +#include "include/aegisub/context.h" +#include "video_frame.h" +#include "libresrc/libresrc.h" +#include "options.h" +#include "project.h" +#include "selection_controller.h" +#include "video_controller.h" +#include "async_video_provider.h" +#include "colour_button.h" +#include "image_position_picker.h" + +#include + +#include +#include + +#include +#include +#include +#if BOOST_VERSION >= 106900 +#include +#else +#include +#endif + +namespace { + class DialogAlignToVideo final : public wxDialog { + agi::Context* context; + AsyncVideoProvider* provider; + + wxImage preview_image; + VideoFrame current_frame; + int current_n_frame; + + ImagePositionPicker* preview_frame; + ColourButton* selected_color; + wxTextCtrl* selected_x; + wxTextCtrl* selected_y; + wxTextCtrl* selected_tolerance; + + void update_from_textbox(); + void update_from_textbox(wxCommandEvent&); + + bool check_exists(int pos, int x, int y, int* lrud, double* orig, unsigned char tolerance); + void process(wxCommandEvent&); + public: + DialogAlignToVideo(agi::Context* context); + ~DialogAlignToVideo(); + }; + + DialogAlignToVideo::DialogAlignToVideo(agi::Context* context) + : wxDialog(context->parent, -1, _("Align subtitle to video by key point"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxMAXIMIZE_BOX | wxRESIZE_BORDER) + , context(context), provider(context->project->VideoProvider()) + { + auto add_with_label = [&](wxSizer * sizer, wxString const& label, wxWindow * ctrl) { + sizer->Add(new wxStaticText(this, -1, label), 0, wxLEFT | wxRIGHT | wxCENTER, 3); + sizer->Add(ctrl, 1, wxLEFT); + }; + + auto tolerance = OPT_GET("Tool/Align to Video/Tolerance")->GetInt(); + auto maximized = OPT_GET("Tool/Align to Video/Maximized")->GetBool(); + + current_n_frame = context->videoController->GetFrameN(); + current_frame = *context->project->VideoProvider()->GetFrame(current_n_frame, 0, true); + preview_image = GetImage(current_frame); + + preview_frame = new ImagePositionPicker(this, preview_image, [&](int x, int y, unsigned char r, unsigned char g, unsigned char b) -> void { + selected_x->ChangeValue(wxString::Format(wxT("%i"), x)); + selected_y->ChangeValue(wxString::Format(wxT("%i"), y)); + + selected_color->SetColor(agi::Color(r, g, b)); + }); + selected_color = new ColourButton(this, wxSize(55, 16), true, agi::Color("FFFFFF")); + selected_color->SetToolTip(_("The key color to be followed.")); + selected_x = new wxTextCtrl(this, -1, "0"); + selected_x->SetToolTip(_("The x coord of the key point.")); + selected_y = new wxTextCtrl(this, -1, "0"); + selected_y->SetToolTip(_("The y coord of the key point.")); + selected_tolerance = new wxTextCtrl(this, -1, wxString::Format(wxT("%i"), int(tolerance))); + selected_tolerance->SetToolTip(_("Max tolerance of the color.")); + + selected_x->Bind(wxEVT_TEXT, &DialogAlignToVideo::update_from_textbox, this); + selected_y->Bind(wxEVT_TEXT, &DialogAlignToVideo::update_from_textbox, this); + update_from_textbox(); + + wxFlexGridSizer* right_sizer = new wxFlexGridSizer(4, 2, 5, 5); + add_with_label(right_sizer, _("X"), selected_x); + add_with_label(right_sizer, _("Y"), selected_y); + add_with_label(right_sizer, _("Color"), selected_color); + add_with_label(right_sizer, _("Tolerance"), selected_tolerance); + right_sizer->AddGrowableCol(1, 1); + + wxSizer* main_sizer = new wxBoxSizer(wxHORIZONTAL); + + main_sizer->Add(preview_frame, 1, (wxALL & ~wxRIGHT) | wxEXPAND, 5); + main_sizer->Add(right_sizer, 0, wxALIGN_LEFT, 5); + + wxSizer* dialog_sizer = new wxBoxSizer(wxVERTICAL); + dialog_sizer->Add(main_sizer, wxSizerFlags(1).Border(wxALL & ~wxBOTTOM).Expand()); + dialog_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Right().Border()); + SetSizerAndFit(dialog_sizer); + SetSize(1024, 700); + CenterOnParent(); + + Bind(wxEVT_BUTTON, &DialogAlignToVideo::process, this, wxID_OK); + SetIcon(GETICON(button_align_16)); + if (maximized) + wxDialog::Maximize(true); + } + + DialogAlignToVideo::~DialogAlignToVideo() + { + long lt; + if (!selected_tolerance->GetValue().ToLong(<)) + return; + if (lt < 0 || lt > 255) + return; + + OPT_SET("Tool/Align to Video/Tolerance")->SetInt(lt); + } + + void rgb2lab(unsigned char r, unsigned char g, unsigned char b, double* lab) + { + double R = static_cast(r) / 255.0; + double G = static_cast(g) / 255.0; + double B = static_cast(b) / 255.0; + double X = 0.412453 * R + 0.357580 * G + 0.180423 * B; + double Y = 0.212671 * R + 0.715160 * G + 0.072169 * B; + double Z = 0.019334 * R + 0.119193 * G + 0.950227 * B; + double xr = X / 0.950456, yr = Y / 1.000, zr = Z / 1.088854; + + if (yr > 0.008856) { + lab[0] = 116.0 * pow(yr, 1.0 / 3.0) - 16.0; + } + else { + lab[0] = 903.3 * yr; + } + + double fxr, fyr, fzr; + if (xr > 0.008856) + fxr = pow(xr, 1.0 / 3.0); + else + fxr = 7.787 * xr + 16.0 / 116.0; + + if (yr > 0.008856) + fyr = pow(yr, 1.0 / 3.0); + else + fyr = 7.787 * yr + 16.0 / 116.0; + + if (zr > 0.008856) + fzr = pow(zr, 1.0 / 3.0); + else + fzr = 7.787 * zr + 16.0 / 116.0; + + lab[1] = 500.0 * (fxr - fyr); + lab[2] = 200.0 * (fyr - fzr); + } + + template + bool check_point(boost::gil::pixel & pixel, double orig[3], unsigned char tolerance) + { + double lab[3]; + // in pixel: B,G,R + rgb2lab(pixel[2], pixel[1], pixel[0], lab); + auto diff = sqrt(pow(lab[0] - orig[0], 2) + pow(lab[1] - orig[1], 2) + pow(lab[2] - orig[2], 2)); + return diff < tolerance; + } + + template + bool calculate_point(boost::gil::image_view view, int x, int y, double orig[3], unsigned char tolerance, int* ret) + { + auto origin = *view.at(x, y); + if (!check_point(origin, orig, tolerance)) + return false; + auto w = view.width(); + auto h = view.height(); + int l = x, r = x, u = y, d = y; + for (int i = x + 1; i < w; i++) + { + auto p = *view.at(i, y); + if (!check_point(p, orig, tolerance)) + { + r = i; + break; + } + } + + for (int i = x - 1; i >= 0; i--) + { + auto p = *view.at(i, y); + if (!check_point(p, orig, tolerance)) + { + l = i; + break; + } + } + + for (int i = y + 1; i < h; i++) + { + auto p = *view.at(x, i); + if (!check_point(p, orig, tolerance)) + { + d = i; + break; + } + } + + for (int i = y - 1; i >= 0; i--) + { + auto p = *view.at(x, i); + if (!check_point(p, orig, tolerance)) + { + u = i; + break; + } + } + ret[0] = l; + ret[1] = r; + ret[2] = u; + ret[3] = d; + return true; + } + + void DialogAlignToVideo::process(wxCommandEvent & evt) + { + auto n_frames = provider->GetFrameCount(); + auto w = provider->GetWidth(); + auto h = provider->GetHeight(); + + long lx, ly, lt; + if (!selected_x->GetValue().ToLong(&lx) || !selected_y->GetValue().ToLong(&ly) || !selected_tolerance->GetValue().ToLong(<)) + { + wxMessageBox(_("Bad x or y position or tolerance value!")); + evt.Skip(); + return; + } + if (lx < 0 || ly < 0 || lx >= w || ly >= h) + { + wxMessageBox(wxString::Format(_("Bad x or y position! Require: 0 <= x < %i, 0 <= y < %i"), w, h)); + evt.Skip(); + return; + } + if (lt < 0 || lt > 255) + { + wxMessageBox(_("Bad tolerance value! Require: 0 <= torlerance <= 255")); + evt.Skip(); + return; + } + int x = int(lx), y = int(ly); + unsigned char tolerance = unsigned char(lt); + + auto color = selected_color->GetColor(); + auto r = color.r; + auto b = color.b; + auto g = color.g; + double lab[3]; + rgb2lab(r, g, b, lab); + + int pos = current_n_frame; + auto frame = provider->GetFrame(pos, -1, true); + auto view = interleaved_view(frame->width, frame->height, reinterpret_cast(frame->data.data()), frame->pitch); + if (frame->flipped) + y = frame->height - y; + int lrud[4]; + calculate_point(view, x, y, lab, tolerance, lrud); + + // find forward +#define CHECK_EXISTS_POS check_exists(pos, x, y, lrud, lab, tolerance) + while (pos >= 0) + { + if (CHECK_EXISTS_POS) + pos -= 2; + else break; + } + pos++; + pos = std::max(0, pos); + auto left = CHECK_EXISTS_POS ? pos : pos + 1; + + pos = current_n_frame; + while (pos < n_frames) + { + if (CHECK_EXISTS_POS) + pos += 2; + else break; + } + pos--; + pos = std::min(pos, n_frames - 1); + auto right = CHECK_EXISTS_POS ? pos : pos - 1; + + auto timecode = context->project->Timecodes(); + auto line = context->selectionController->GetActiveLine(); + line->Start = timecode.TimeAtFrame(left); + line->End = timecode.TimeAtFrame(right + 1); // exclusive + context->ass->Commit(_("Align to video by key point"), AssFile::COMMIT_DIAG_TIME); + Close(); + } + + + + bool DialogAlignToVideo::check_exists(int pos, int x, int y, int* lrud, double* orig, unsigned char tolerance) + { + auto frame = provider->GetFrame(pos, -1, true); + auto view = interleaved_view(frame->width, frame->height, reinterpret_cast(frame->data.data()), frame->pitch); + if (frame->flipped) + y = frame->height - y; + int actual[4]; + if (!calculate_point(view, x, y, orig, tolerance, actual)) return false; + int dl = abs(actual[0] - lrud[0]); + int dr = abs(actual[1] - lrud[1]); + int du = abs(actual[2] - lrud[2]); + int dd = abs(actual[3] - lrud[3]); + + return dl <= 5 && dr <= 5 && du <= 5 && dd <= 5; + } + + void DialogAlignToVideo::update_from_textbox() + { + long lx, ly; + int w = preview_image.GetWidth(), h = preview_image.GetHeight(); + if (!selected_x->GetValue().ToLong(&lx) || !selected_y->GetValue().ToLong(&ly)) + return; + + if (lx < 0 || ly < 0 || lx >= w || ly >= h) + return; + int x = int(lx); + int y = int(ly); + auto r = preview_image.GetRed(x, y); + auto g = preview_image.GetGreen(x, y); + auto b = preview_image.GetBlue(x, y); + selected_color->SetColor(agi::Color(r, g, b)); + } + + void DialogAlignToVideo::update_from_textbox(wxCommandEvent & evt) + { + update_from_textbox(); + } + +} + + +void ShowAlignToVideoDialog(agi::Context * c) +{ + c->dialog->Show(c); +} diff --git a/src/dialogs.h b/src/dialogs.h index 2e03d09e9..d781c6614 100644 --- a/src/dialogs.h +++ b/src/dialogs.h @@ -74,3 +74,4 @@ void ShowSpellcheckerDialog(agi::Context *c); void ShowStyleManagerDialog(agi::Context *c); void ShowTimingProcessorDialog(agi::Context *c); void ShowVideoDetailsDialog(agi::Context *c); +void ShowAlignToVideoDialog(agi::Context* c); diff --git a/src/hotkey.cpp b/src/hotkey.cpp index 2ec8f50e4..deb624628 100644 --- a/src/hotkey.cpp +++ b/src/hotkey.cpp @@ -29,6 +29,11 @@ #include namespace { + const char* added_hotkeys_cj[][3] = { + {"time/align", "Video", "KP_TAB"}, + {nullptr} + }; + const char *added_hotkeys_7035[][3] = { {"audio/play/line", "Audio", "R"}, {nullptr} @@ -81,6 +86,11 @@ void init() { auto migrations = OPT_GET("App/Hotkey Migrations")->GetListString(); + if (boost::find(migrations, "cj") == end(migrations)) { + migrate_hotkeys(added_hotkeys_cj); + migrations.emplace_back("cj"); + } + if (boost::find(migrations, "7035") == end(migrations)) { migrate_hotkeys(added_hotkeys_7035); migrations.emplace_back("7035"); diff --git a/src/image_position_picker.cpp b/src/image_position_picker.cpp new file mode 100644 index 000000000..2bc9c6f89 --- /dev/null +++ b/src/image_position_picker.cpp @@ -0,0 +1,127 @@ +#include "image_position_picker.h" +BEGIN_EVENT_TABLE(ImagePositionPicker, wxPanel) + // some useful events + /* + EVT_MOTION(ImagePositionPicker::mouseMoved) + EVT_LEFT_DOWN(ImagePositionPicker::mouseDown) + EVT_LEFT_UP(ImagePositionPicker::mouseReleased) + EVT_RIGHT_DOWN(ImagePositionPicker::rightClick) + EVT_LEAVE_WINDOW(ImagePositionPicker::mouseLeftWindow) + EVT_KEY_DOWN(ImagePositionPicker::keyPressed) + EVT_KEY_UP(ImagePositionPicker::keyReleased) + EVT_MOUSEWHEEL(ImagePositionPicker::mouseWheelMoved) + */ + + // catch paint events + EVT_PAINT(ImagePositionPicker::paintEvent) + //Size event + EVT_SIZE(ImagePositionPicker::OnSize) + EVT_MOUSE_EVENTS(ImagePositionPicker::OnMouseEvent) +END_EVENT_TABLE() + + +// some useful events +/* + void ImagePositionPicker::mouseMoved(wxMouseEvent& event) {} + void ImagePositionPicker::mouseDown(wxMouseEvent& event) {} + void ImagePositionPicker::mouseWheelMoved(wxMouseEvent& event) {} + void ImagePositionPicker::mouseReleased(wxMouseEvent& event) {} + void ImagePositionPicker::rightClick(wxMouseEvent& event) {} + void ImagePositionPicker::mouseLeftWindow(wxMouseEvent& event) {} + void ImagePositionPicker::keyPressed(wxKeyEvent& event) {} + void ImagePositionPicker::keyReleased(wxKeyEvent& event) {} + */ + +ImagePositionPicker::ImagePositionPicker(wxWindow* parent, wxImage i, updator upd) : wxPanel(parent) +{ + image = i; + prevW = -1; + prevH = -1; + w = image.GetWidth(); + h = image.GetHeight(); + update = upd; +} + +/* + * Called by the system of by wxWidgets when the panel needs + * to be redrawn. You can also trigger this call by + * calling Refresh()/Update(). + */ + +void ImagePositionPicker::paintEvent(wxPaintEvent& evt) +{ + // depending on your system you may need to look at double-buffered dcs + wxPaintDC dc(this); + render(dc); +} + +/* + * Alternatively, you can use a clientDC to paint on the panel + * at any time. Using this generally does not free you from + * catching paint events, since it is possible that e.g. the window + * manager throws away your drawing when the window comes to the + * background, and expects you will redraw it when the window comes + * back (by sending a paint event). + */ +void ImagePositionPicker::paintNow() +{ + // depending on your system you may need to look at double-buffered dcs + wxClientDC dc(this); + render(dc); +} + +/* + * Here we do the actual rendering. I put it in a separate + * method so that it can work no matter what type of DC + * (e.g. wxPaintDC or wxClientDC) is used. + */ +void ImagePositionPicker::render(wxDC& dc) +{ + int neww, newh; + dc.GetSize(&neww, &newh); + + if (neww != prevW || newh != prevH) + { + // keep the image proportionate + int ww, hh; + if (double(neww) / w >= double(newh) / h) // too long + { + ww = newh * w / h; + hh = newh; + } + else + { + ww = neww; + hh = neww * h / w; + } + resized = wxBitmap(image.Scale(ww, hh /*, wxIMAGE_QUALITY_HIGH*/)); + prevW = ww; + prevH = hh; + dc.DrawBitmap(resized, 0, 0, false); + } + else { + dc.DrawBitmap(resized, 0, 0, false); + } +} + +/* + * Here we call refresh to tell the panel to draw itself again. + * So when the user resizes the image panel the image should be resized too. + */ +void ImagePositionPicker::OnSize(wxSizeEvent& event) { + Refresh(); + //skip the event. + event.Skip(); +} + +void ImagePositionPicker::OnMouseEvent(wxMouseEvent& evt) +{ + wxPoint pos = evt.GetPosition(); + if (evt.Dragging() || evt.LeftDown() || evt.LeftUp()) + { + int x = pos.x * w / prevW; + int y = pos.y * h / prevH; + if (x >= 0 && x < w && y >= 0 && y < h) + update(x, y, image.GetRed(x, y), image.GetGreen(x, y), image.GetBlue(x, y)); + } +} diff --git a/src/image_position_picker.h b/src/image_position_picker.h new file mode 100644 index 000000000..1f808db39 --- /dev/null +++ b/src/image_position_picker.h @@ -0,0 +1,37 @@ +#include +#include +#include "gl_wrap.h" + +typedef std::function updator; + +class ImagePositionPicker : public wxPanel +{ + wxImage image; + wxBitmap resized; + int prevW, prevH, w, h; + + updator update; + +public: + ImagePositionPicker(wxWindow* parent, wxImage i, updator upd); + + void paintEvent(wxPaintEvent & evt); + void paintNow(); + void OnSize(wxSizeEvent& event); + void OnMouseEvent(wxMouseEvent& evt); + void render(wxDC& dc); + + // some useful events + /* + void mouseMoved(wxMouseEvent& event); + void mouseDown(wxMouseEvent& event); + void mouseWheelMoved(wxMouseEvent& event); + void mouseReleased(wxMouseEvent& event); + void rightClick(wxMouseEvent& event); + void mouseLeftWindow(wxMouseEvent& event); + void keyPressed(wxKeyEvent& event); + void keyReleased(wxKeyEvent& event); + */ + + DECLARE_EVENT_TABLE() +}; diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 318f8d3ee..c86ef3609 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -575,6 +575,10 @@ }, "Visual" : { "Autohide": false + }, + "Align to Video" : { + "Tolerance" : 20, + "Maximized" : true } }, diff --git a/src/libresrc/default_hotkey.json b/src/libresrc/default_hotkey.json index b9460979e..d4b0af376 100644 --- a/src/libresrc/default_hotkey.json +++ b/src/libresrc/default_hotkey.json @@ -286,6 +286,9 @@ ], "video/frame/prev/large" : [ "Alt-Left" + ], + "time/align" : [ + "KP_TAB" ] }, "Translation Assistant" : { diff --git a/src/libresrc/default_toolbar.json b/src/libresrc/default_toolbar.json index 07afb90b0..7b85ee600 100644 --- a/src/libresrc/default_toolbar.json +++ b/src/libresrc/default_toolbar.json @@ -42,6 +42,7 @@ "subtitle/select/visible", "time/snap/scene", "time/frame/current", + "time/align", "", "tool/style/manager", "subtitle/properties", diff --git a/src/meson.build b/src/meson.build index 72587d366..495c3f418 100644 --- a/src/meson.build +++ b/src/meson.build @@ -56,6 +56,7 @@ aegisub_src = files( 'context.cpp', 'crash_writer.cpp', 'dialog_about.cpp', + 'dialog_align.cpp', 'dialog_attachments.cpp', 'dialog_automation.cpp', 'dialog_autosave.cpp', @@ -97,6 +98,7 @@ aegisub_src = files( 'help_button.cpp', 'hotkey.cpp', 'hotkey_data_view_model.cpp', + 'image_position_picker.cpp', 'initial_line_state.cpp', 'main.cpp', 'menu.cpp', From 37380658bbec3481df344f588c3c1fb5ab86f1f8 Mon Sep 17 00:00:00 2001 From: wangqr Date: Thu, 9 May 2019 16:48:15 -0400 Subject: [PATCH 02/10] Fix errors in AlignToVideo * Call TimeAtFrame with correct parameter * Fix syntax error --- src/dialog_align.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dialog_align.cpp b/src/dialog_align.cpp index 004408bfa..5243d2197 100644 --- a/src/dialog_align.cpp +++ b/src/dialog_align.cpp @@ -281,7 +281,7 @@ namespace { return; } int x = int(lx), y = int(ly); - unsigned char tolerance = unsigned char(lt); + unsigned char tolerance = (unsigned char)(lt); auto color = selected_color->GetColor(); auto r = color.r; @@ -323,8 +323,8 @@ namespace { auto timecode = context->project->Timecodes(); auto line = context->selectionController->GetActiveLine(); - line->Start = timecode.TimeAtFrame(left); - line->End = timecode.TimeAtFrame(right + 1); // exclusive + line->Start = timecode.TimeAtFrame(left, agi::vfr::Time::START); + line->End = timecode.TimeAtFrame(right, agi::vfr::Time::END); // exclusive context->ass->Commit(_("Align to video by key point"), AssFile::COMMIT_DIAG_TIME); Close(); } From 2b3df72c7598db8ad6f0b0b023a966755db095dc Mon Sep 17 00:00:00 2001 From: wangqr Date: Mon, 9 Mar 2020 21:02:33 -0400 Subject: [PATCH 03/10] Fix missing config option for Align on macOS --- src/libresrc/osx/default_config.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 59f2ed05f..56e9ec4e6 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -575,6 +575,10 @@ }, "Visual" : { "Autohide": false + }, + "Align to Video" : { + "Tolerance" : 20, + "Maximized" : true } }, From 60dcc35830a5fdfab9aa3cdeeb2522ed5d540c95 Mon Sep 17 00:00:00 2001 From: wangqr Date: Sat, 24 Aug 2019 02:10:03 -0400 Subject: [PATCH 04/10] Remove the trailing period in help text of time/align Fix wangqr/Aegisub#7 --- src/command/time.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command/time.cpp b/src/command/time.cpp index eec19c2d4..c44216db2 100644 --- a/src/command/time.cpp +++ b/src/command/time.cpp @@ -240,7 +240,7 @@ struct time_align_subtitle_to_point final : public validate_video_loaded { CMD_ICON(button_align) STR_MENU("Align subtitle to video") STR_DISP("Align subtitle to video") - STR_HELP("Align subtitle to video by key points.") + STR_HELP("Align subtitle to video by key points") void operator()(agi::Context* c) override { c->videoController->Stop(); ShowAlignToVideoDialog(c); From b2ba1feb285a8d2ee01e63fd166f8563e42889b6 Mon Sep 17 00:00:00 2001 From: wangqr Date: Wed, 25 Dec 2019 18:38:42 -0500 Subject: [PATCH 05/10] Submit "align to video" on double click Fix wangqr/Aegisub#34 --- src/dialog_align.cpp | 8 +++----- src/image_position_picker.cpp | 5 +++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/dialog_align.cpp b/src/dialog_align.cpp index 5243d2197..2de54b2c0 100644 --- a/src/dialog_align.cpp +++ b/src/dialog_align.cpp @@ -76,7 +76,7 @@ namespace { void update_from_textbox(wxCommandEvent&); bool check_exists(int pos, int x, int y, int* lrud, double* orig, unsigned char tolerance); - void process(wxCommandEvent&); + void process(wxEvent&); public: DialogAlignToVideo(agi::Context* context); ~DialogAlignToVideo(); @@ -137,6 +137,7 @@ namespace { CenterOnParent(); Bind(wxEVT_BUTTON, &DialogAlignToVideo::process, this, wxID_OK); + Bind(wxEVT_LEFT_DCLICK, &DialogAlignToVideo::process, this, preview_frame->GetId()); SetIcon(GETICON(button_align_16)); if (maximized) wxDialog::Maximize(true); @@ -255,7 +256,7 @@ namespace { return true; } - void DialogAlignToVideo::process(wxCommandEvent & evt) + void DialogAlignToVideo::process(wxEvent &) { auto n_frames = provider->GetFrameCount(); auto w = provider->GetWidth(); @@ -265,19 +266,16 @@ namespace { if (!selected_x->GetValue().ToLong(&lx) || !selected_y->GetValue().ToLong(&ly) || !selected_tolerance->GetValue().ToLong(<)) { wxMessageBox(_("Bad x or y position or tolerance value!")); - evt.Skip(); return; } if (lx < 0 || ly < 0 || lx >= w || ly >= h) { wxMessageBox(wxString::Format(_("Bad x or y position! Require: 0 <= x < %i, 0 <= y < %i"), w, h)); - evt.Skip(); return; } if (lt < 0 || lt > 255) { wxMessageBox(_("Bad tolerance value! Require: 0 <= torlerance <= 255")); - evt.Skip(); return; } int x = int(lx), y = int(ly); diff --git a/src/image_position_picker.cpp b/src/image_position_picker.cpp index 2bc9c6f89..5912b4fc3 100644 --- a/src/image_position_picker.cpp +++ b/src/image_position_picker.cpp @@ -124,4 +124,9 @@ void ImagePositionPicker::OnMouseEvent(wxMouseEvent& evt) if (x >= 0 && x < w && y >= 0 && y < h) update(x, y, image.GetRed(x, y), image.GetGreen(x, y), image.GetBlue(x, y)); } + else if (evt.LeftDClick()) { + // Propagate the double click event to submit + evt.ResumePropagation(wxEVENT_PROPAGATE_MAX); + evt.Skip(); + } } From 2300988613206b31b2fa5410d240b98dbed95956 Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 16 Jul 2020 23:04:26 +0200 Subject: [PATCH 06/10] dialog_align: Remove point at the end of the tooltips No other tooltips end with points, even if they're a sentence.These ones aren't even sentences. Also updates all *.po files accordingly, with an hacked in POT-Creation-Date due to issues with make_pot.sh. --- src/dialog_align.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dialog_align.cpp b/src/dialog_align.cpp index 2de54b2c0..2c40eedbf 100644 --- a/src/dialog_align.cpp +++ b/src/dialog_align.cpp @@ -105,13 +105,13 @@ namespace { selected_color->SetColor(agi::Color(r, g, b)); }); selected_color = new ColourButton(this, wxSize(55, 16), true, agi::Color("FFFFFF")); - selected_color->SetToolTip(_("The key color to be followed.")); + selected_color->SetToolTip(_("The key color to be followed")); selected_x = new wxTextCtrl(this, -1, "0"); - selected_x->SetToolTip(_("The x coord of the key point.")); + selected_x->SetToolTip(_("The x coord of the key point")); selected_y = new wxTextCtrl(this, -1, "0"); - selected_y->SetToolTip(_("The y coord of the key point.")); + selected_y->SetToolTip(_("The y coord of the key point")); selected_tolerance = new wxTextCtrl(this, -1, wxString::Format(wxT("%i"), int(tolerance))); - selected_tolerance->SetToolTip(_("Max tolerance of the color.")); + selected_tolerance->SetToolTip(_("Max tolerance of the color")); selected_x->Bind(wxEVT_TEXT, &DialogAlignToVideo::update_from_textbox, this); selected_y->Bind(wxEVT_TEXT, &DialogAlignToVideo::update_from_textbox, this); From f1cfd480c4430bb3197834535c3faa5e3857db93 Mon Sep 17 00:00:00 2001 From: Oneric Date: Fri, 17 Jul 2020 17:09:05 +0200 Subject: [PATCH 07/10] AlignToVideo: Improve rgb->lab precision --- src/dialog_align.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/dialog_align.cpp b/src/dialog_align.cpp index 2c40eedbf..dafc59972 100644 --- a/src/dialog_align.cpp +++ b/src/dialog_align.cpp @@ -156,12 +156,9 @@ namespace { void rgb2lab(unsigned char r, unsigned char g, unsigned char b, double* lab) { - double R = static_cast(r) / 255.0; - double G = static_cast(g) / 255.0; - double B = static_cast(b) / 255.0; - double X = 0.412453 * R + 0.357580 * G + 0.180423 * B; - double Y = 0.212671 * R + 0.715160 * G + 0.072169 * B; - double Z = 0.019334 * R + 0.119193 * G + 0.950227 * B; + double X = (0.412453 * r + 0.357580 * g + 0.180423 * b) / 255.0; + double Y = (0.212671 * r + 0.715160 * g + 0.072169 * b) / 255.0; + double Z = (0.019334 * r + 0.119193 * g + 0.950227 * b) / 255.0; double xr = X / 0.950456, yr = Y / 1.000, zr = Z / 1.088854; if (yr > 0.008856) { From 57841a0a34f9e54acde01bcc7d4672561c430a89 Mon Sep 17 00:00:00 2001 From: Oneric Date: Thu, 16 Jul 2020 23:21:25 +0200 Subject: [PATCH 08/10] AlignToVideo: Handle tolerance = 0 correctly Checking if the diff is '>' instead of '>=' will always fail if tolerance=0, even if the colours are identical. This will cause the line to get a startime greter than its end time, which is not desireable. Rounding errors and limits of floating type precision might still affect the comparison. An additional sanity check before calculation is added to ensure the selected position and colour match within tolerance. This allows us to refactor the search code to never check the starting frame and guanrantees valid timings with start @@ -290,28 +290,30 @@ namespace { auto view = interleaved_view(frame->width, frame->height, reinterpret_cast(frame->data.data()), frame->pitch); if (frame->flipped) y = frame->height - y; + + // Ensure selected color and position match + if(!check_point(*view.at(x,y), lab, tolerance)) + { + wxMessageBox(_("Selected position and color are not within tolerance!")); + return; + } + int lrud[4]; calculate_point(view, x, y, lab, tolerance, lrud); // find forward #define CHECK_EXISTS_POS check_exists(pos, x, y, lrud, lab, tolerance) - while (pos >= 0) - { - if (CHECK_EXISTS_POS) - pos -= 2; - else break; - } + do { + pos -= 2; + } while (pos >= 0 && !CHECK_EXISTS_POS) pos++; pos = std::max(0, pos); auto left = CHECK_EXISTS_POS ? pos : pos + 1; pos = current_n_frame; - while (pos < n_frames) - { - if (CHECK_EXISTS_POS) - pos += 2; - else break; - } + do { + pos += 2; + } while (pos < n_frames && !CHECK_EXISTS_POS) pos--; pos = std::min(pos, n_frames - 1); auto right = CHECK_EXISTS_POS ? pos : pos - 1; From d75e49653ae425ea0c28c4331fefe2d8574a8fae Mon Sep 17 00:00:00 2001 From: wangqr Date: Fri, 17 Jul 2020 12:47:06 -0400 Subject: [PATCH 09/10] Fix syntax error in e2ea84541f806310c1c7b37cb6e38569cf845488 --- src/dialog_align.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dialog_align.cpp b/src/dialog_align.cpp index d6d755352..93e506636 100644 --- a/src/dialog_align.cpp +++ b/src/dialog_align.cpp @@ -305,7 +305,7 @@ namespace { #define CHECK_EXISTS_POS check_exists(pos, x, y, lrud, lab, tolerance) do { pos -= 2; - } while (pos >= 0 && !CHECK_EXISTS_POS) + } while (pos >= 0 && CHECK_EXISTS_POS); pos++; pos = std::max(0, pos); auto left = CHECK_EXISTS_POS ? pos : pos + 1; @@ -313,7 +313,7 @@ namespace { pos = current_n_frame; do { pos += 2; - } while (pos < n_frames && !CHECK_EXISTS_POS) + } while (pos < n_frames && CHECK_EXISTS_POS); pos--; pos = std::min(pos, n_frames - 1); auto right = CHECK_EXISTS_POS ? pos : pos - 1; From b2fbc84335d6d4df2cc804ea3dddff42a7fc753d Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Tue, 23 Aug 2022 19:30:41 +0200 Subject: [PATCH 10/10] AlignToVideo: Reexport the new icon files Some metadata of these files was triggering the assertion errors in #6 . Reexporting the icons using ffmpeg fixed this. Fixes #6 . --- src/bitmaps/button/button_align_16.png | Bin 353 -> 280 bytes src/bitmaps/button/button_align_24.png | Bin 571 -> 454 bytes src/bitmaps/button/button_align_32.png | Bin 690 -> 526 bytes src/bitmaps/button/button_align_48.png | Bin 1161 -> 952 bytes src/bitmaps/button/button_align_64.png | Bin 547 -> 489 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/bitmaps/button/button_align_16.png b/src/bitmaps/button/button_align_16.png index 07ce86c1b127a1cb5711bebf1b67ccb99b70623d..307547b5a2f38a02e8c37c59376832e9be25f225 100644 GIT binary patch delta 252 zcmV|xn4Kphu7+|1v=9ZhcZf>5{0c6iVa1*RBKO28N7`Y`hx&d|-pQ`ozOuAd!0@8_vGNp`o`V94P(u|9=q%hQ@h|>dR__ zoK1lu*S_Mi;md>Vrw~4L)Df3gR7xr7LG|-sOiKXPu!kyzF}HL80000pF_DMuRR5(v#{Qv(y11W$BN)rtRR>RvXhq$@9?|z4JApoIaR%Ik3 z47APKdh7PBAE=?VROX-(21W6n_*KlYz*B0XtL$7$7uk zn^p#t+_hyfkUVnf)El7CgQGz9V~8{iAT$7hw~iQ4g|uyWjD;ivgV4+CM}XXO@1e2~ zfTH2UL40e=#t8 z{r_K{k#XnV6+hc=+`fEv2T+`o9U>0{P>n$F<-zt-2p>A?h|4P~rIhqQ`7ki(5+4Ht Y0L=CAten63umAu607*qoM6N<$g7629UH||9 diff --git a/src/bitmaps/button/button_align_24.png b/src/bitmaps/button/button_align_24.png index 8e98fee9d1bde949352a94bca3d67b2a0df1fb90..b7c3f9813732353cf3078accdbd0560d5c2db3fc 100644 GIT binary patch delta 428 zcmV;d0aO0F1jYl9B!3BTNLh0L01m?d01m?duxOa_0004aNkl_20s?ZU4WV-Y6#>_ z&a?xuh4eN|tA8iap#Q&ASixZ`ZUN!*0y!f3^IJoai%VFSN^eI@_;_1ljh-|?s!(&Z7I3<3b~ WwWo%(3B$7h0000pG&`Cr=R5(v#{Qv(y12q5>l%^Pr8blb7V9@p5OFZ1&<7%fu`KSOr z`gbjxy#MMec2;H}`~Sbc|5(JD>vADdCuSB~X7n)_Mg2d!6n`Ru0)Pe~gLor$s00*< zt=_$7#j<5<*Kev%@&@u*l`OX%JPlQi1keq7_V~`89ed7RxU_9<9Z(w&hXNzmJQTpH z167R#&<$EOAq}YV%Fq9g*0%#0JbEGLj%{DKXvxAwON(Ocfg&tY+AB63fT~6U=z-1n z{v(idbxquT{(mxv;qGx_b^p09Dsizxm?w1d1riiz?4`>!BT8~}?!zWM@peuEglU!KqktbYbFE?+qL|Mdl3351ui0I17g zAl_LCB3@FS2Vrz?zYdYwJ24B$Wi^e4aM1v|K@~yTK-CFNYk}-yM^Rp*;7`Bqs|v{& znHckdQagi$V=6=?8o&tb+Z()c=Rw&ppb0Vo0(f`b`V19C1w<9Ke}4b?_4_|FGb1}Y k2UG|Hj2o8Oga88r05Rypb5DeCr~m)}07*qoM6N<$g656wq5uE@ diff --git a/src/bitmaps/button/button_align_32.png b/src/bitmaps/button/button_align_32.png index 958f4b34eb382f97943c26277c79d85bcf642221..da4be9273d5047eadba37953c446693077b3606e 100644 GIT binary patch delta 500 zcmVDp>IU|3PF6G8RAm|Nj*uj0?13bAc^|7X07Q>I^eb>?c^{ z04(T$>1IY*Af*;$sPMymFyRPP(32a)E!%o;XSp*)7HqGvKuhb7!Q!7!EZKVJ-+v0@ z;82e@118YUMOL#q-=16xzU@II9;;>`AI@z!{}-+*No^M@SiR>R$v#Lofkdr0g!^!D zI*_*vl(Da1dTi|zJm%xD;OpLM1_tXt*ra^LP>t8f!Dl?qNdEH~mxd#2r<7OL)YjJa q_4Vx9c>}-cxGkU_7`0#oSO5S9R1gi;Ri35*0000pHM@d9MR7gu>{Qv(y11$j)l%^RNwSXWPwSXWXKObD$I(NyD`%Hhn zd1SXcD6m6CF#*s5^1!?VJ%|YvyWc~FumS8rq{4^=sK(5PN`HfafPerR11rEDM7oTO zSD;J;un8&iHsty7>o1Va#>stl&E$1w9s}8S(M8^FE@g3^U{&Y5!nR}#RuR?h}=8Qy*Q4q<$H{RzUr4xkoc01r;60tn#p zD=yZ8vfaB50u?ODw1RLO_J4$E!3yx?gM$$L5RkF>>3{$KuNa{`2A~a_3v3~5{1)IT zD(rT&LPME>L2P1-6a$0kerV7!*fZ)rWfOt0@dJEi(T4eFpe%-0(~p8E`||Cc+#p8T z)=i3xc!M4epgzF>+iNVKDq!F-Sp4&eC0phc?T832C(}e-2|*v%p1aexHuiiTL#M5 zm_-!B+9wcI*a3D6zV5ANV6grJ<)HvyF{rsvppgTWzyWYnIDfw|F!Mm!sNm?jshiKd zU}Iqv6y(>@_HlJnhf3f8qb)mvVAKKvfPn!3a1)~klIB)rD*ylh07*qoM6N<$g8OqO AumAu6 diff --git a/src/bitmaps/button/button_align_48.png b/src/bitmaps/button/button_align_48.png index 9d28cffad34b14a970c6f66148f86730efd2ce38..c6075451b86e3cb0e87a9abf572d6c6de767850c 100644 GIT binary patch delta 930 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