Example #1
0
        private static void AddKaraokeEffects(AssLine originalLine, AssLine stepLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx)
        {
            int prevNumActiveActions          = stepIdx > 0 ? activeSectionsPerStep.Values[stepIdx - 1] : 0;
            int numActiveSections             = activeSectionsPerStep.Values[stepIdx];
            List <AssSection> singingSections = stepLine.Sections
                                                .Cast <AssSection>()
                                                .Skip(prevNumActiveActions)
                                                .Take(numActiveSections - prevNumActiveActions)
                                                .ToList();

            switch (stepLine.KaraokeType)
            {
            case KaraokeType.Simple:
                ApplySimpleKaraokeEffect(singingSections);
                break;

            case KaraokeType.Fade:
                ApplyFadeKaraokeEffect(originalLine, stepLine, activeSectionsPerStep, stepIdx, singingSections);
                break;

            case KaraokeType.Glitch:
                ApplyGlitchKaraokeEffect(stepLine, singingSections);
                break;
            }
        }
Example #2
0
        public override object Clone()
        {
            AssLine newLine = new AssLine(Start, End);

            newLine.Assign(this);
            return(newLine);
        }
Example #3
0
        // Aegisub doesn't display the background box for certain parts of a subtitle:
        // - Whitespace at the start of a line of text
        // - Whitespace at the end of a line of text
        // - Sections that consist only of whitespace
        // YouTube displays the background box across the entire subtitle, however,
        // so for an accurate visualization, we need to enclose whitespace blocks in
        // other characters to "protect" them.
        private static void ProtectWhitespace(AssLine line)
        {
            for (int i = line.Sections.Count - 1; i >= 0; i--)
            {
                AssSection section = (AssSection)line.Sections[i];
                if (Regex.IsMatch(section.Text, @"^[\r\n]+$"))
                {
                    continue;
                }

                Match match = Regex.Match(section.Text, @"^([ \xA0]*)(.*?)([ \xA0]*)$");
                if (match.Groups[1].Length == section.Text.Length)
                {
                    // If the entire section is just whitespace, we have to use a non-whitespace character to make its background visible
                    section.Text      = CreateProtectedWhitespaceString(section.Text.Length, ".");
                    section.ForeColor = ColorUtil.ChangeColorAlpha(section.ForeColor, 0);
                    section.ShadowColors.Clear();
                }
                else
                {
                    // Otherwise, we can use non-breaking spaces (which don't normally work, but do work in our case because we later write
                    // them out using Aegisub's \h sequence)
                    section.Text = CreateProtectedWhitespaceString(match.Groups[1].Length, "\xA0") + match.Groups[2].Value + CreateProtectedWhitespaceString(match.Groups[3].Length, "\xA0");
                }
            }
        }
Example #4
0
        private IEnumerable <AssLine> ApplyKaraokeType(AssLine originalLine, AssLine stepLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx)
        {
            int prevNumActiveActions          = stepIdx > 0 ? activeSectionsPerStep.Values[stepIdx - 1] : 0;
            int numActiveSections             = activeSectionsPerStep.Values[stepIdx];
            List <AssSection> singingSections = stepLine.Sections
                                                .Cast <AssSection>()
                                                .Skip(prevNumActiveActions)
                                                .Take(numActiveSections - prevNumActiveActions)
                                                .ToList();

            AssKaraokeStepContext context =
                new AssKaraokeStepContext
            {
                Document              = this,
                OriginalLine          = originalLine,
                ActiveSectionsPerStep = activeSectionsPerStep,

                StepLine          = stepLine,
                StepIndex         = stepIdx,
                NumActiveSections = numActiveSections,
                SingingSections   = singingSections
            };

            return(stepLine.KaraokeType.Apply(context));
        }
Example #5
0
        private static AssLine CreateKaraokeStepLine(AssLine originalLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx)
        {
            TimeSpan timeOffset        = activeSectionsPerStep.Keys[stepIdx];
            int      numActiveSections = activeSectionsPerStep.Values[stepIdx];

            DateTime startTime = TimeUtil.SnapTimeToFrame((originalLine.Start + timeOffset).AddMilliseconds(20));

            if (startTime >= originalLine.End)
            {
                return(null);
            }

            DateTime endTime;

            if (stepIdx < activeSectionsPerStep.Count - 1)
            {
                endTime = TimeUtil.SnapTimeToFrame((originalLine.Start + activeSectionsPerStep.Keys[stepIdx + 1]).AddMilliseconds(20)).AddMilliseconds(-1);
                if (endTime > originalLine.End)
                {
                    endTime = originalLine.End;
                }
            }
            else
            {
                endTime = originalLine.End;
            }

            AssLine stepLine = (AssLine)originalLine.Clone();

            stepLine.Start = startTime;
            stepLine.End   = endTime;

            foreach (AssSection section in stepLine.Sections.Take(numActiveSections))
            {
                section.Animations.RemoveAll(a => a is SecondaryColorAnimation);
            }

            foreach (AssSection section in stepLine.Sections.Skip(numActiveSections))
            {
                section.ForeColor = section.SecondaryColor;

                section.Animations.RemoveAll(a => a is ForeColorAnimation);
                foreach (SecondaryColorAnimation anim in section.Animations.OfType <SecondaryColorAnimation>().ToList())
                {
                    section.Animations.Remove(anim);
                    section.Animations.Add(new ForeColorAnimation(anim.StartTime, anim.StartColor, anim.EndTime, anim.EndColor));
                }

                if (section.ForeColor.A == 0 && !section.Animations.OfType <ForeColorAnimation>().Any())
                {
                    section.ForeColor = Color.FromArgb(0, 0, 0, 0);
                    section.BackColor = Color.FromArgb(0, 0, 0, 0);
                    section.ShadowColors.Clear();
                }
            }

            AddKaraokeEffects(originalLine, stepLine, activeSectionsPerStep, stepIdx);
            return(stepLine);
        }
        private static void ApplyFadeKaraokeEffect(AssLine originalLine, AssLine stepLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx)
        {
            int        numActiveSections = activeSectionsPerStep.Values[stepIdx];
            AssSection singingSection    = (AssSection)stepLine.Sections[numActiveSections - 1];

            ApplyFadeInKaraokeEffect(stepLine, singingSection);
            ApplyFadeOutKaraokeEffect(originalLine, stepLine, activeSectionsPerStep, stepIdx);
        }
        private static void InsertRubySection(AssLine line, Section format, string text, RubyPart rubyPart, int sectionIndex, ref int numSubSections)
        {
            Section section = (Section)format.Clone();

            section.Text     = text;
            section.RubyPart = rubyPart;
            line.Sections.Insert(sectionIndex + numSubSections, section);
            numSubSections++;
        }
Example #8
0
        private static void ApplyNativeKaraoke(AssLine line)
        {
            TimeSpan timeOffset = TimeSpan.Zero;

            foreach (AssSection section in line.Sections)
            {
                section.StartOffset = timeOffset;
                timeOffset         += section.Duration;
            }
        }
Example #9
0
        private AssLine CreateTextVisualizationLine(AssLine originalLine)
        {
            AssLine textLine = (AssLine)originalLine.Clone();

            foreach (AssSection textSection in textLine.Sections)
            {
                textSection.BackColor = Color.Empty;
                textSection.ShadowColors.Clear();
            }
            return(textLine);
        }
Example #10
0
 private static bool CanUseNativeKaraoke(AssLine line)
 {
     return(line.KaraokeType.GetType() == typeof(SimpleKaraokeType) &&
            line.Animations.Count == 0 &&
            line.Sections.Cast <AssSection>().All(s => s.SecondaryColor.A == 0 &&
                                                  s.CurrentWordForeColor.IsEmpty &&
                                                  s.CurrentWordOutlineColor.IsEmpty &&
                                                  s.CurrentWordShadowColor.IsEmpty &&
                                                  s.Animations.Count == 0 &&
                                                  s.Duration != TimeSpan.Zero));
 }
Example #11
0
        protected override void Assign(Line line)
        {
            base.Assign(line);

            AssLine assLine = (AssLine)line;

            Alpha = assLine.Alpha;
            Animations.Clear();
            Animations.AddRange(assLine.Animations);
            KaraokeType = assLine.KaraokeType;
        }
Example #12
0
        internal void CreateTagSections(AssLine line, string text, AssTagContext context)
        {
            text = Regex.Replace(text, @"(?:\\N)+$", "");
            HashSet <string> handledWholeLineTags = new HashSet <string>();

            int start = 0;

            foreach (Match tagGroupMatch in Regex.Matches(text, @"\{(.*?)\}"))
            {
                int end = tagGroupMatch.Index;

                if (end > start || (context.Section.Duration > TimeSpan.Zero && Regex.IsMatch(tagGroupMatch.Groups[1].Value, @"\\k\s*\d+")))
                {
                    if (end == start)
                    {
                        context.Section.Text = "\x200B";
                    }
                    else
                    {
                        context.Section.Text = text.Substring(start, end - start).Replace("\\N", "\r\n");
                    }

                    line.Sections.Add(context.Section);

                    context.Section          = (AssSection)context.Section.Clone();
                    context.Section.Text     = null;
                    context.Section.Duration = TimeSpan.Zero;
                }

                foreach (Match tagMatch in Regex.Matches(tagGroupMatch.Groups[1].Value, @"\\(?<tag>fn|\d?[a-z]+)\s*(?<arg>\([^\(\)]*(?:\)|$)|[^\\\(\)]*)"))
                {
                    if (!_tagHandlers.TryGetValue(tagMatch.Groups["tag"].Value, out AssTagHandlerBase handler))
                    {
                        continue;
                    }

                    if (handler.AffectsWholeLine && !handledWholeLineTags.Add(tagMatch.Groups["tag"].Value))
                    {
                        continue;
                    }

                    handler.Handle(context, tagMatch.Groups["arg"].Value.Trim());
                }

                start = tagGroupMatch.Index + tagGroupMatch.Length;
            }

            if (start < text.Length)
            {
                context.Section.Text = text.Substring(start, text.Length - start).Replace("\\N", "\r\n");
                line.Sections.Add(context.Section);
            }
        }
Example #13
0
        private AssLine CreateBackgroundVisualizationLine(AssLine originalLine)
        {
            AssLine backgroundLine = (AssLine)originalLine.Clone();

            foreach (AssSection section in backgroundLine.Sections)
            {
                section.ForeColor      = Color.Empty;
                section.SecondaryColor = Color.Empty;
                section.ShadowColors.Clear();
            }
            return(backgroundLine);
        }
Example #14
0
        private IEnumerable <AssLine> CreateShadowVisualizationLines(AssLine originalLine)
        {
            if (originalLine.Sections.Any(s => s.ShadowColors.ContainsKey(ShadowType.SoftShadow)))
            {
                for (float blur = 4; blur >= 2; blur--)
                {
                    yield return(CreateShadowVisualizationLine(originalLine, s => s.ShadowColors.GetOrDefault(ShadowType.SoftShadow), 2, blur));
                }
            }

            if (originalLine.Sections.Any(s => s.ShadowColors.ContainsKey(ShadowType.HardShadow)))
            {
                for (int offset = 3; offset >= 1; offset--)
                {
                    yield return(CreateShadowVisualizationLine(originalLine, s => s.ShadowColors.GetOrDefault(ShadowType.HardShadow), offset, 0));
                }
            }

            if (originalLine.Sections.Any(s => s.ShadowColors.ContainsKey(ShadowType.Bevel)))
            {
                yield return(CreateShadowVisualizationLine(originalLine, s => s.ShadowColors.GetOrDefault(ShadowType.Bevel), -1, 0));

                yield return(CreateShadowVisualizationLine(
                                 originalLine,
                                 s =>
                {
                    Color color = s.ShadowColors.GetOrDefault(ShadowType.Bevel);
                    if (color.IsEmpty)
                    {
                        return Color.Empty;
                    }

                    if (color.R == 0x22 && color.G == 0x22 & color.B == 0x22)
                    {
                        return Color.FromArgb(color.A, 0xCC, 0xCC, 0xCC);
                    }

                    return color;
                },
                                 1,
                                 0
                                 ));
            }

            if (originalLine.Sections.Any(s => s.ShadowColors.ContainsKey(ShadowType.Glow)))
            {
                for (float blur = 2; blur >= 1; blur -= 0.5f)
                {
                    yield return(CreateShadowVisualizationLine(originalLine, s => s.ShadowColors.GetOrDefault(ShadowType.Glow), 0, blur));
                }
            }
        }
Example #15
0
        private static IEnumerable <AssLine> CreateEmulatedKaraokeLines(AssLine line)
        {
            SortedList <TimeSpan, int> activeSectionsPerStep = GetKaraokeSteps(line);

            for (int stepIdx = 0; stepIdx < activeSectionsPerStep.Count; stepIdx++)
            {
                AssLine stepLine = CreateKaraokeStepLine(line, activeSectionsPerStep, stepIdx);
                if (stepLine != null)
                {
                    yield return(stepLine);
                }
            }
        }
        private static void ApplyGlitchKaraokeEffect(AssLine stepLine, AssSection singingSection)
        {
            if (singingSection.Text.Length == 0)
            {
                return;
            }

            ApplySimpleKaraokeEffect(singingSection);
            DateTime glitchEndTime = TimeUtil.Min(stepLine.Start.AddMilliseconds(70), stepLine.End);

            Util.CharacterRange[] charRanges = GetGlitchKaraokeCharacterRanges(singingSection.Text[0]);
            singingSection.Animations.Add(new GlitchingCharAnimation(stepLine.Start, glitchEndTime, charRanges));
        }
Example #17
0
        private IEnumerable <AssLine> CreateEmulatedKaraokeLines(AssLine line)
        {
            SortedList <TimeSpan, int> activeSectionsPerStep = GetKaraokeSteps(line);

            for (int stepIdx = 0; stepIdx < activeSectionsPerStep.Count; stepIdx++)
            {
                IEnumerable <AssLine> stepLines = CreateKaraokeStepLines(line, activeSectionsPerStep, stepIdx);
                foreach (AssLine stepLine in stepLines)
                {
                    yield return(stepLine);
                }
            }
        }
Example #18
0
        protected IEnumerable <AssLine> CreateEmulatedKaraokeLines(AssLine line)
        {
            SortedList <TimeSpan, int> activeSectionsPerStep = GetKaraokeSteps(line);

            for (int stepIdx = 0; stepIdx < activeSectionsPerStep.Count; stepIdx++)
            {
                IEnumerable <AssLine> stepLines = CreateKaraokeStepLines(line, activeSectionsPerStep, stepIdx);
                foreach (AssLine stepLine in stepLines)
                {
                    stepLine.Sections.RemoveAll(s => s.Text == "\x200B");       // Remove empty sections that were added in CreateTagSections()
                    yield return(stepLine);
                }
            }
        }
Example #19
0
        /// <summary>
        /// Unlike YouTube, where the native karaoke feature hides the unsung parts completely (text/background box/shadow),
        /// the equivalent in Aegisub (\k with a transparent secondary color) only hides the text. This means we need to
        /// switch to emulated karaoke (duplicated lines without \k) for visual correctness.
        /// </summary>
        private void EmulateKaraokeForLinesWithBackgroundOrShadow()
        {
            for (int i = 0; i < Lines.Count; i++)
            {
                AssLine line = (AssLine)Lines[i];
                if (line.Sections.Cast <AssSection>().All(s => s.Duration == TimeSpan.Zero) ||
                    line.Sections.All(s => s.BackColor.A == 0 && s.ShadowColors.Count == 0))
                {
                    continue;
                }

                Lines.RemoveAt(i);
                Lines.InsertRange(i, CreateEmulatedKaraokeLines(line));
            }
        }
Example #20
0
        private static void ApplyGlitchKaraokeEffect(AssLine stepLine, List <AssSection> singingSections)
        {
            AssSection singingSection = singingSections.LastOrDefault(s => s.RubyPart == RubyPart.None || s.RubyPart == RubyPart.Text);

            if (singingSection == null || singingSection.Text.Length == 0)
            {
                return;
            }

            ApplySimpleKaraokeEffect(singingSections);
            DateTime glitchEndTime = TimeUtil.Min(stepLine.Start.AddMilliseconds(70), stepLine.End);

            Util.CharacterRange[] charRanges = GetGlitchKaraokeCharacterRanges(singingSection.Text[0]);
            singingSection.Animations.Add(new GlitchingCharAnimation(stepLine.Start, glitchEndTime, charRanges));
        }
Example #21
0
        private List <AssLine> ParseLine(AssDialogue dialogue, AssStyle style, AssStyleOptions styleOptions)
        {
            AssLine line =
                new AssLine(
                    TimeUtil.RoundTimeToFrameCenter(dialogue.Start),
                    TimeUtil.RoundTimeToFrameCenter(dialogue.End)
                    )
            {
                AnchorPoint = style.AnchorPoint
            };

            string[] effects = dialogue.Effect.Split(';');
            if (effects.Contains(EffectNames.NoAndroidDarkTextHack))
            {
                line.AndroidDarkTextHackAllowed = false;
            }

            AssTagContext context = new AssTagContext
            {
                Document            = this,
                InitialStyle        = style,
                InitialStyleOptions = styleOptions,
                Style        = style,
                StyleOptions = styleOptions,
                Line         = line,
                Section      = new AssSection()
            };

            ApplyStyle(context.Section, style, styleOptions);
            CreateTagSections(line, dialogue.Text, context);
            CreateRubySections(line);

            List <AssLine> lines = new List <AssLine> {
                line
            };

            foreach (AssTagContext.PostProcessor postProcessor in context.PostProcessors)
            {
                List <AssLine> extraLines = postProcessor();
                if (extraLines != null)
                {
                    lines.AddRange(extraLines);
                }
            }

            return(lines);
        }
Example #22
0
        private IEnumerable <AssLine> ExpandLineForKaraoke(AssLine line)
        {
            if (line.Sections.Cast <AssSection>().All(s => s.Duration == TimeSpan.Zero))
            {
                return new[] { line }
            }
            ;

            if (CanUseNativeKaraoke(line))
            {
                ApplyNativeKaraoke(line);

                return(new[] { line });
            }

            return(CreateEmulatedKaraokeLines(line));
        }
Example #23
0
        private void AppendLineTags(AssLine line, AssStyle style, AssLineContentBuilder lineContent)
        {
            if (line.AnchorPoint != style.AnchorPoint)
            {
                lineContent.AppendTag("an", GetAlignment(line.AnchorPoint));
            }

            if (line.Position != null)
            {
                lineContent.AppendTag("pos", line.Position.Value.X, line.Position.Value.Y);
            }

            if (line.VerticalTextType != VerticalTextType.None)
            {
                lineContent.AppendTag("ytvert", AssVerticalTypeTagHandler.GetVerticalTextTypeId(line.VerticalTextType));
            }
        }
Example #24
0
        private static void CreateRubySections(AssLine line)
        {
            for (int sectionIdx = line.Sections.Count - 1; sectionIdx >= 0; sectionIdx--)
            {
                AssSection section = (AssSection)line.Sections[sectionIdx];
                if (section.RubyPosition == RubyPosition.None)
                {
                    continue;
                }

                MatchCollection matches = Regex.Matches(section.Text, @"\[(?<text>.+?)/(?<ruby>.+?)\]");
                if (matches.Count == 0)
                {
                    continue;
                }

                line.Sections.RemoveAt(sectionIdx);

                int interStartPos  = 0;
                int numSubSections = 0;
                foreach (Match match in matches)
                {
                    if (match.Index > interStartPos)
                    {
                        InsertRubySection(line, section, section.Text.Substring(interStartPos, match.Index - interStartPos), RubyPart.None, sectionIdx, ref numSubSections);
                    }

                    InsertRubySection(line, section, match.Groups["text"].Value, RubyPart.Text, sectionIdx, ref numSubSections);
                    InsertRubySection(line, section, "(", RubyPart.Parenthesis, sectionIdx, ref numSubSections);
                    InsertRubySection(line, section, match.Groups["ruby"].Value, section.RubyPosition == RubyPosition.Below ? RubyPart.RubyBelow : RubyPart.RubyAbove, sectionIdx, ref numSubSections);
                    InsertRubySection(line, section, ")", RubyPart.Parenthesis, sectionIdx, ref numSubSections);

                    interStartPos = match.Index + match.Length;
                }

                if (interStartPos < section.Text.Length)
                {
                    InsertRubySection(line, section, section.Text.Substring(interStartPos), RubyPart.None, sectionIdx, ref numSubSections);
                }

                ((AssSection)line.Sections[sectionIdx]).Duration = section.Duration;
            }
        }
Example #25
0
        private AssLine CreateShadowVisualizationLine(AssLine originalLine, Func <AssSection, Color> getShadowColor, float positionOffset, float blur)
        {
            AssLine shadowLine = (AssLine)originalLine.Clone();

            if (positionOffset != 0)
            {
                PointF position = shadowLine.Position ?? GetDefaultPosition(shadowLine.AnchorPoint);
                shadowLine.Position = new PointF(position.X + positionOffset, position.Y + positionOffset);
            }

            foreach (AssSection shadowSection in shadowLine.Sections)
            {
                shadowSection.ForeColor = getShadowColor(shadowSection);
                shadowSection.BackColor = Color.Empty;
                shadowSection.ShadowColors.Clear();
                shadowSection.Blur = blur;
            }
            return(shadowLine);
        }
        private static IEnumerable <AssLine> ExpandLineForKaraoke(AssLine line)
        {
            if (line.Sections.Cast <AssSection>().All(s => s.Duration == TimeSpan.Zero))
            {
                yield return(line);

                yield break;
            }

            SortedList <TimeSpan, int> activeSectionsPerStep = GetKaraokeSteps(line);

            for (int stepIdx = 0; stepIdx < activeSectionsPerStep.Count; stepIdx++)
            {
                AssLine stepLine = CreateKaraokeStepLine(line, activeSectionsPerStep, stepIdx);
                if (stepLine != null)
                {
                    yield return(stepLine);
                }
            }
        }
        private static void AddKaraokeEffects(AssLine originalLine, AssLine stepLine, SortedList <TimeSpan, int> activeSectionsPerStep, int stepIdx)
        {
            int        numActiveSections = activeSectionsPerStep.Values[stepIdx];
            AssSection singingSection    = (AssSection)stepLine.Sections[numActiveSections - 1];

            switch (stepLine.KaraokeType)
            {
            case KaraokeType.Simple:
                ApplySimpleKaraokeEffect(singingSection);
                break;

            case KaraokeType.Fade:
                ApplyFadeKaraokeEffect(originalLine, stepLine, activeSectionsPerStep, stepIdx);
                break;

            case KaraokeType.Glitch:
                ApplyGlitchKaraokeEffect(stepLine, singingSection);
                break;
            }
        }
        private static void MoveLineBreaksToSeparateSections(AssLine line)
        {
            for (int sectionIdx = line.Sections.Count - 1; sectionIdx >= 0; sectionIdx--)
            {
                AssSection section = (AssSection)line.Sections[sectionIdx];
                Match      match   = Regex.Match(section.Text, @"((?:\r\n)+|[^\r\n]+)+");
                if (!match.Success || match.Groups[1].Captures.Count == 1)
                {
                    continue;
                }

                line.Sections.RemoveAt(sectionIdx);
                for (int i = 0; i < match.Groups[1].Captures.Count; i++)
                {
                    AssSection subSection = (AssSection)section.Clone();
                    subSection.Text = match.Groups[1].Captures[i].Value;
                    line.Sections.Insert(sectionIdx + i, subSection);
                }
            }
        }
Example #29
0
        // Aegisub's background boxes can have padding just like on YouTube, and the horizontal and vertical padding
        // can even be different. The big downside of this feature, however, is that background boxes of adjacent
        // sections overlap each other which looks like a mess. Therefore we don't use it at all (all the styles
        // have an outline thickness of 0.01) and instead add a space at the start and end of each line of text
        // to emulate the YouTube horizontal padding manually.
        private static void EmulateBorders(AssLine line)
        {
            for (int i = 0; i < line.Sections.Count; i++)
            {
                AssSection section = (AssSection)line.Sections[i];
                if (Regex.IsMatch(section.Text, @"^[\r\n]+$"))
                {
                    continue;
                }

                if (i == 0 || line.Sections[i - 1].Text.EndsWith("\r\n"))
                {
                    section.Text = " " + section.Text;
                }

                if (i == line.Sections.Count - 1 || line.Sections[i + 1].Text.StartsWith("\r\n"))
                {
                    section.Text = section.Text + " ";
                }
            }
        }
Example #30
0
        private void AppendLineTags(AssLine line, AssStyle style, AssLineContentBuilder lineContent)
        {
            if (line.AnchorPoint != style.AnchorPoint)
            {
                lineContent.AppendTag("an", GetAlignment(line.AnchorPoint));
            }

            if (line.Position != null)
            {
                lineContent.AppendTag("pos", line.Position.Value.X, line.Position.Value.Y);
            }

            if (line.VerticalTextType != VerticalTextType.None)
            {
                lineContent.AppendTag("ytvert", AssVerticalTypeTagHandler.GetVerticalTextTypeId(line.HorizontalTextDirection, line.VerticalTextType));
            }
            else if (line.HorizontalTextDirection != HorizontalTextDirection.LeftToRight)
            {
                lineContent.AppendTag("ytdir", AssHorizontalTextDirectionTag.GetHorizontalTextDirectionId(line.HorizontalTextDirection));
            }
        }